﻿
Type.registerNamespace('TallComponents');

///////////////////////// Size object /////////////////////

TallComponents.Size = function (width, height) {
   this.width = width;
   this.height = height;
   this.initialize();
};

TallComponents.Size.prototype = {
   initialize: function () {
   },

   convertToVector2: function () {
      return new TallComponents.Vector2(this.width, this.height);
   },

   clone: function () {
      return new TallComponents.Size(this.width, this.height);
   },

   _getInfoAsString: function () {
      return String.format('({0}, {1})', Math.round(this.width), Math.round(this.height));
   },

   _showInfo: function () {
      alert(String.format('Size: {0}', this._getInfoAsString()));
   }
};

///////////////////////// Vector2 object /////////////////////

TallComponents.Vector2 = function (x, y) {
   this.x = x;
   this.y = y;
   this.initialize();
};

TallComponents.Vector2.prototype = {
   initialize: function () {
   },

   add: function (vector) {
      this.x += vector.x;
      this.y += vector.y;
   },

   substract: function (vector) {
      this.x -= vector.x;
      this.y -= vector.y;
   },

   multiplyByNumber: function (num) {
      this.x *= num;
      this.y *= num;
   },

   devideByNumber: function (num) {
      this.x = this.x / num;
      this.y = this.y / num;
   },

   clone: function () {
      return new TallComponents.Vector2(this.x, this.y);
   },

   _getInfoAsString: function () {
      return String.format('({0}, {1})', Math.round(this.x), Math.round(this.y));
   },

   _showInfo: function () {
      alert(String.format('Vector2: {0}', this._getInfoAsString()));
   }
};

///////////////////////// Rectangle object /////////////////////

TallComponents.Rectangle = function (location, size) {
   this.location = location;
   this.size = size;

   this.initialize();
};

TallComponents.Rectangle.prototype = {
   initialize: function () {
   },

   get_leftTop: function () {
      return this.location;
   },

   get_rightBottom: function () {
      var location = this.location.clone();
      location.add(this.size.convertToVector2());
      return location;
   },

   contains: function (obj) {
      
      if (obj instanceof TallComponents.Vector2) {
         return this._containsPoint(obj);
      }

      if (obj instanceof TallComponents.Rectangle){
         return this._containsRectangle(obj);
      }
   },

   scale: function (factor) {
      this.size.width *= factor;
      this.size.height *= factor;
   },

   translateToGlobal: function (parentRectangle) {
      this.location.x = parentRectangle.location.x + this.location.x;
      this.location.y = (parentRectangle.size.height - this.location.y) + parentRectangle.location.y;
   },

   rotateToParent: function (angle, parentRectangle) {
      ///<summary>Rotates the rectangle to the specified angle.</summary>
      ///<remarks>The center of the parentRectangle is assumed as the origin for the rotation.</remarks>
      if (angle != null) {

         switch (angle) {
            case 90:
               var x = this.location.x;
               this.location.x = this.location.y - this.size.height;
               this.location.y = parentRectangle.size.height - x;

               var width = this.size.width;
               this.size.width = this.size.height;
               this.size.height = width;
               break;
            case 180:
               this.location.x = parentRectangle.size.width - this.location.x - this.size.width;
               this.location.y = parentRectangle.size.height - this.location.y + this.size.height;
               break;
            case 270:
               var x = this.location.x;
               this.location.x = parentRectangle.size.width - this.location.y;
               this.location.y = x + this.size.width;

               var width = this.size.width;
               this.size.width = this.size.height;
               this.size.height = width;
               break;
            default:
               break;
         }
      }
   },

   clone: function () {
      var location = this.location.clone();
      var size = this.size.clone();
      var clone = new TallComponents.Rectangle(location, size);
      return clone;
   },

   _containsPoint: function (vector) {
      //to avoid rounding errors. 2 pixels is not much
      var accuracyOffs = 1;
      return ((this.location.x-accuracyOffs < vector.x+accuracyOffs && vector.x-accuracyOffs < this.get_rightBottom().x+accuracyOffs) &&
                      (this.location.y-accuracyOffs < vector.y+accuracyOffs && vector.y-accuracyOffs < this.get_rightBottom().y+accuracyOffs))
   },

   _containsRectangle: function (rectangle) {
        return (this.contains(rectangle.location) && this.contains(rectangle.get_rightBottom()));
   },

   _getInfoAsString: function () {

      var rsw = Math.round(this.size.width);
      var rsh = Math.round(this.size.height);

      var locInfo = this.location._getInfoAsString();
      var sizeInfo = this.size._getInfoAsString();
      var leftTopInfo = this.get_leftTop()._getInfoAsString();
      var rightBottomInfo = this.get_rightBottom()._getInfoAsString();

      return String.format('location: {0}\nSize: ({1})\n\nleftTop: {2}\nbottomRight: {3}',
                              locInfo,
                              sizeInfo,
                              leftTopInfo,
                              rightBottomInfo);
   },

   _showInfo: function () {
      alert(String.format('rectangle\n{0}', this._getInfoAsString()));
   }
};

///////////////////////// Zoom object /////////////////////

TallComponents.Zoom = function (element, viewport) {

   this._container = element;
   this._viewport = viewport;

   this._stringValue = null;
   this._value = 1;

   this._zoomPosition = new TallComponents.Vector2(0, 0);
   this._isUpdated = true;
   this._oldValue = NaN;
   this._oldStringValue = NaN;

   this.initialize();
};

TallComponents.Zoom.prototype = {

   initialize: function () {
   },

   calculateZoomValue: function (pageSize) {
      ///<summary>Gets the zoom value in percents for the specified page size.</summary>
      var zoom = this._internalGetValue(pageSize);

      if (!isNaN(zoom)) {
         this._value = zoom;
      } else {
         this._value = this._oldValue;
         this._stringValue = this._oldStringValue;
      }
      return this._value;
   },

   setValue: function (value) {
      ///<summary>Sets the zoom value</summary>
      this._internalSetValue(value, value + '', null);
   },

   getValueAsString: function () {
      ///<summary>Gets the zoom string value('page', 'width' or percents)</summary>
      return this._stringValue;
   },

   zoomIn: function (position) {
      ///<summary>Performs zoom in to the specified position. Requires to call a 'markDocumentPositionAsNotAffected' method when document's position is set.</summary>
      var zoom = Math.min(10, parseFloat(this._value) + 0.25).toFixed(2);
      var zoomString = zoom + '';
      this._internalSetValue(zoom, zoomString, position);
   },

   zoomOut: function (position) {
      ///<summary>Performs zoom out. Requires to call a 'markDocumentPositionAsNotAffected' method when document's position is set.</summary>
      var zoom = Math.max(0.25, parseFloat(this._value) - 0.25).toFixed(2);
      var zoomString = zoom + '';
      this._internalSetValue(zoom, zoomString, position);
   },

   isDocumentPositionAffectedByZoom: function () {
      ///<summary>Indicates whether the position of the document was affected by the zoom operation.</summary>
      return ((!this._isUpdated) && (this._zoomPosition != null));
   },

   markDocumentPositionAsNotAffected: function () {
      ///<summary>Marks the zoom object as updated. It implies that the document has been already set to a particular position.</summary>
      this._isUpdated = true;
   },

   calculateDocumentPosition: function () {
      ///<summary>Calculates a new document's position after it have been resized(zoomed).</summary>
      if (this._isUpdated || this._zoomPosition == null) {
         return null;
      }
      var zoomPosition = this._zoomPosition.clone();

      //convert position's coordinates from viewport space to container space
      var documentPosition = this._viewport.get_position();
      zoomPosition.substract(documentPosition);

      //and take the new zoom factor into account
      var resizeFactor = this._value / this._oldValue;
      zoomPosition.multiplyByNumber(resizeFactor);

      //get the center position of the viewport 
      var minViewportSize = this._viewport.get_minSize();
      var viewportCenter = minViewportSize.convertToVector2();
      viewportCenter.devideByNumber(2);

      zoomPosition.substract(viewportCenter);
      zoomPosition.multiplyByNumber(-1);

      return zoomPosition;
   },

   _normalizeStringZoomValue: function (value) {
      ///<summary>Normalizes the string zoom value</summary>
      ///only two values can be converted to numeric representation: 'page' & 'width'
      var stringValue = (value + '').toLowerCase();

      switch (stringValue) {
         case 'page':
         case 'fittopage':
            stringValue = 'page';
            break;
         case 'width':
         case 'fittowidth':
            stringValue = 'width';
            break;
         default:
            break;
      }

      return stringValue;
   },

   _tryConvertStringToZoomInPercent: function (zoomAsString, pageSize) {
      ///<summary>Converts predefined val ue of the zoom (such as 'page') to the numerical representation.</summary>
      var minViewportSize = this._viewport.get_minSize()

      switch (zoomAsString) {
         case 'page':
            //I use the min size because the normal one makes scrollbars appear 
            //in double pages modes even if they are unnecessary.
            var w = minViewportSize.width / pageSize.width;
            var h = minViewportSize.height / pageSize.height;

            return Math.min(w, h); ;
         case 'width':
            //I use the min size because the normal one makes scrollbars appear 
            //in double pages modes even if they are unnecessary.
            return minViewportSize.width / pageSize.width;
         default:
            return NaN;
      }
   },

   _internalGetValue: function (pageSize) {
      ///<summary>Gets the zoom value in numeric representation</summary>
      if (pageSize.width < 1 || pageSize.height < 1) {
         return NaN;
      }

      //try to parse value
      var value = this._stringValue;
      var zoomValue = parseFloat(value);

      if (!isNaN(zoomValue)) {
         //it is a numeric value
         return zoomValue;
      }
      else {
         //it is one of the predefined constant values
         //in order to convert this string value to a numeric representation,
         //perform some normalization of the value
         var stringValue = this._normalizeStringZoomValue(value);

         //try to convert the constant to a numeric value
         zoomValue = this._tryConvertStringToZoomInPercent(stringValue, pageSize);

         if (!isNaN(zoomValue)) {
            //the constant was successfully converted to numeric value
            return zoomValue;
         }
         else {
            //attempt to convert unknown constant value
            //do nothing
         }
      }
      return NaN;
   },

   _internalSetValue: function (value, stringValue, position) {
      ///<summary>Sets the zoom values and a position of the point to which the zoom operation was applied</summary>

      //save the old string value
      this._oldStringValue = this._stringValue;
      //normalize a new value and make it current
      this._stringValue = this._normalizeStringZoomValue(stringValue);

      //save the old zoom value and set a new zoom value
      this._oldValue = this._value;
      this._value = value;

      //save a position of the point to which the zoom operation was applied
      this._zoomPosition = position;
      //mark the zoom object as not updated
      this._isUpdated = false;
   }
};
///////////////////////// End Zoom  object /////////////////////


///////////////////////// Image object /////////////////////

TallComponents.Image = function (element) {
   this._container = element;
   this.initialize();
};

TallComponents.Image.prototype = {

   initialize: function () {
   },

   parseImageProperties: function (url) {
      ///<summary>Parses a given URL and returns an object which contains an information about the image.</summary>
      var result = new Object();
      if (url.indexOf('url(') === 0) {
         url = url.substring(4, url.length - 1);
      }
      result.url = url;
      var trim = url.indexOf('?');
      if (trim >= 0) {
         url = url.substring(trim + 1, url.length);
      }

      var vars = url.split("&");
      for (var i = 0; i < vars.length; i++) {
         var pair = vars[i].split("=");
         switch (pair[0]) {
            case 'a':
               result.pageIndex = parseInt(pair[1]);
               break;
            case 'b':
               result.key = pair[1];
               break;
            case 'c':
               result.zoom = parseFloat(pair[1]);
               break;
            case 'f':
               result.width = parseInt(pair[1]);
               break;
            case 'g':
               result.height = parseInt(pair[1]);
               break;
            case 'j':
               result.verticalSpacing = parseInt(pair[1]);
               break;
            case 'h':
               result.horisontalSpacing = parseInt(pair[1]);
               break;
            case 'r':
               result.rotation = parseInt(pair[1]);
               break;
            case 'l':
               result.pageLayout = pair[1];
               break;
            default:
               break;
      }
      }
      return result;
   },

   calculateSize: function (containerSize, minViewportSize, maxViewportSize) {
      ///<summary>Calculates the size of the visible area of the specified page at the specified zoom factor.</summary>
      ///<remarks>The size is limited by the page size at the specified zoom and the size of the view port.</remarks>
      var imageSize = containerSize.clone();
      var maxSize = maxViewportSize.clone();

      if (imageSize.height > maxViewportSize.height) {
         // scroll vertically
         maxSize.width = minViewportSize.width;
      }
      if (imageSize.width > maxViewportSize.width) {
         // scroll horizontally
         maxSize.height = minViewportSize.height;
      }

      imageSize.width = Math.min(maxSize.width, imageSize.width);
      imageSize.height = Math.min(maxSize.height, imageSize.height);
      return imageSize;
   }
};

///////////////////////// End Image object /////////////////////

///////////////////////// Cursor object /////////////////////

TallComponents.Cursor = function (element) {
   this._container = element;
   this._cursors = null;
   this._absoluteMousePosition = new TallComponents.Vector2(0, 0);
   this._relativeMousePosition = new TallComponents.Vector2(0, 0);
   this.initialize();
};

TallComponents.Cursor.prototype = {

   initialize: function () {
      this._cursorMode = 'pan';
      this._cursors = null;
      this._isMouseDown = false;
      this._mouseDownPosition = null;

      this._documentPositionOnPanStarted = null;
   },

   get_mode: function () {
      ///<summary>Returns the mode of the cursor.</summary>
      return this._cursorMode;
   },

   set_mode: function (value) {
      ///<summary>Sets the mode of the cursor.</summary>
      if (this._cursorMode != value) {
         this._cursorMode = value;

         this._update();
      }
   },

   get_cursors: function () {
      ///<summary>Gets or sets the list of defined cursors.</summary>
      ///<value type="Array" elementType="String">The list of cursors.</value>
      ///<remarks>This is an associative array and is initialized by the server control.
      ///<para>Valid indexes are <c>panUp</c>, <c>panDown</c>, <c>zoomIn</c> and <c>zoomOut</c>.
      ///Each value holds a url for a custom cursor image.
      ///<c>panUp</c> is used when <see cref="M:J#TallComponents.PdfViewer.cursorMode"/> is <c>pan</c> and the mouse button is up.
      ///<c>panDown</c> is used when the mouse is down and the mouse is dragging the PDF preview. <c>zoomIn</c> and <c>zoomOut</c> are used in 
      ///the corresponding cursor modes.</para>
      ///<para>Custom cursors may not be supported in all browsers (Opera 9.64 and earlier do not support this).</para>
      ///</remarks>
      return this._cursors;
   },

   set_cursors: function (value) {
      if (this._cursors != value) {
         this._cursors = value;
      }
   },

   isPan: function () {
      ///<summary>Returns 'true' if the current cursor mode is 'Pan'</summary>
      return this._cursorMode == 'pan';
   },

   isZoomIn: function () {
      ///<summary>Returns 'true' if the current cursor mode is 'ZoomIn'</summary>
      return this._cursorMode == 'zoomIn';
   },

   isZoomOut: function () {
      ///<summary>Returns 'true' if the current cursor mode is 'ZoomOut'</summary>
      return this._cursorMode == 'zoomOut';
   },

   isMouseDown: function () {
      ///<summary>Returns 'true' if the mouse button is pressed down</summary>
      return this._isMouseDown;
   },

   onMouseChanged: function (e) {
      if (!e) { e = window.event; }

      var element = this._container;
      var absolutePosition = new TallComponents.Vector2(0, 0);

      if (e.pageX || e.pageY) {
         absolutePosition.x = e.pageX;
         absolutePosition.y = e.pageY;
      }
      else if (e.clientX || e.clientY) {
         absolutePosition.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
         absolutePosition.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
      }
      this._absoluteMousePosition = absolutePosition;

      var relativePosition = absolutePosition.clone();

      if (element.offsetParent) {
         do {
            relativePosition.x -= element.offsetLeft;
            relativePosition.y -= element.offsetTop;
            element = element.offsetParent;
         } while (element);
      }
      this._relativeMousePosition = relativePosition;
   },

   get_absoluteMousePosition: function () {
      ///<summary>Returns the absolute mouse position.</summary>
      return this._absoluteMousePosition;
   },

   get_relativeMousePosition: function () {
      ///<summary>Returns the relative mouse position.</summary>
      return this._relativeMousePosition;
   },

   get_mouseDownPosition: function () {
      ///<summary>Gets the position where the mouse was down.</summary>
      var position = this._documentPositionOnPanStarted.clone();
      position.substract(this._mouseDownPosition);
      return position;
   },

   startPan: function (mousePosition, documentPosition) {
      ///<summary>Starts to pan.</summary>
      this._isMouseDown = true;

      this._mouseDownPosition = mousePosition;
      this._documentPositionOnPanStarted = documentPosition;

      this._update();
   },

   endPan: function () {
      ///<summary>Stops pan.</summary>
      this._isMouseDown = false;
      this._update();
   },

   _update: function () {
      ///<summary>Updates the visual cursor appearance.</summary>
      if (this.isPan()) {
         this._container.style.cursor = String.format('url({0}), move', this._cursors[this._isMouseDown ? 'panDown' : 'panUp']);
      }
      else if (this.isZoomIn()) {
         this._container.style.cursor = String.format('url({0}), default', this._cursors['zoomIn']);
      }
      else if (this.isZoomOut()) {
         this._container.style.cursor = String.format('url({0}), default', this._cursors['zoomOut']);
      }
   }
};

///////////////////////// End cursor object /////////////////////


///////////////////// Background object ///////////////

TallComponents.Background = function (element) {
   //the grey area behind the image
   this._background = element.parentNode.getElementsByTagName('div')[0];
   if (this._background == element) {
      this._background = null;
   }
};

TallComponents.Background.prototype = {

   initialize: function () {
   },

   setSize: function (width, height) {
      ///<summary>Changes the size of the background.</summary>
      if (this._background) {
         this._background.style.width = width + "px";
         this._background.style.height = height + "px";
      }
   }
};

////////////////// End background object //////////////////


///////////////////// Scrollbars object ///////////////

TallComponents.Scrollbars = function (element) {
   this._viewportElement = element.parentNode;
   this._viewerContainer = element;
};

TallComponents.Scrollbars.prototype = {

   initialize: function () {
      this._isHorisontalEnabled = false;
      this._isVerticalEnabled = false;
   },

   get_position: function () {
      ///<summary>Gets the scrollbars positions.</summary>
      var position = new TallComponents.Vector2(0, 0);

      position.y = (this._isVerticalEnabled) ? -1 * this._viewportElement.scrollTop : parseInt(this._viewerContainer.style.top);
      position.x = (this._isHorisontalEnabled) ? -1 * this._viewportElement.scrollLeft : parseInt(this._viewerContainer.style.left);

      return position;
   },

   set_position: function (position) {
      ///<summary>Sets the position of the scrollbars.</summary>
      this.set_positionX(position.x);
      this.set_positionY(position.y);
   },

   set_positionX: function (xPosition) {
      ///<summary>Sets the position of the horisontal scrollbar.</summary>
      
      if (this._isHorisontalEnabled) {
         this._viewerContainer.style.left = '0px';
         this._viewportElement.scrollLeft = -1 * xPosition;
      }
      else {
         this._viewerContainer.style.left = xPosition + 'px';
      }
   },

   set_positionY: function (yPosition) {
      ///<summary>Sets the position of the vertical scrollbar.</summary>
      if (this._isVerticalEnabled) {
         this._viewerContainer.style.top = '0px';
         this._viewportElement.scrollTop = -1 * yPosition;
      }
      else {
         this._viewerContainer.style.top = yPosition + 'px';
      }
   },

   enableHorisontal: function (enabled) {
      ///<summary>Shows the horisontal scrollbar.</summary>
      this._isHorisontalEnabled = enabled;
      this._viewportElement.style.overflowX = enabled ? 'auto' : 'hidden';
   },

   enableVertical: function (enabled) {
      ///<summary>Shows the vertical scrollbar.</summary>
      this._isVerticalEnabled = enabled;
      this._viewportElement.style.overflowY = enabled ? 'auto' : 'hidden';
   },

   get_isHorisontalEnabled: function () {
      ///<summary>Returns 'true' if the horisontal scrollbar is enabled and 'false' otherwise.</summary>
      return this._isHorisontalEnabled;
   },

   get_isVerticalEnabled: function () {
      ///<summary>Returns 'true' if the vertical scrollbar is enabled and 'false' otherwise.</summary>
      return this._isVerticalEnabled;
   },

   _enable: function (enabled) {
      ///<summary>Shows or hides scrollbars depending on the parametr value.</summary>
      this._viewportElement.style.overflow = enabled ? 'auto' : 'hidden';
      }
};

////////////////// End scrollbars object //////////////////

///////////////////// Viewport object ///////////////

TallComponents.Viewport = function (element, scrollbars, documentContainer) {
   this._viewportElement = element.parentNode;
   this._scrollbars = scrollbars;
   this._documentContainer = documentContainer;

   this._min_viewsize = null;
};

TallComponents.Viewport.prototype = {

   initialize: function () {
   },

   get_position: function () {
      ///<summary>Gets or sets the position of the viewport.</summary>
      return this._scrollbars.get_position();
   },

   set_position: function (position) {
      this.set_positionX(position.x);
      this.set_positionY(position.y);
   },

   set_positionX: function (xPosition) {
      this._scrollbars.set_positionX(xPosition);
   },

   set_positionY: function (yPosition) {
      this._scrollbars.set_positionY(yPosition);
   },

   move: function (position) {
      ///<summary>Moves the viewport to thr specified position.</summary>
      var minViewportSize = this.get_minSize();
      var containerSize = this._documentContainer.get_size();

      var maxX = minViewportSize.width - containerSize.width;
      var maxY = minViewportSize.height - containerSize.height;

      //prevent a document to be moved too far (in both the directions)
      //prevent X
      if (position.x > 0) { position.x = 0; }
      if (position.x < maxX) { position.x = maxX; }
      //prevent Y
      if (position.y > 0) { position.y = 0; }
      if (position.y < maxY) { position.y = maxY; }

      position.x = Math.round(position.x);
      position.y = Math.round(position.y);

      this.set_position(position);
   },

   get_rectangle: function () {
      var viewportPosition = this.get_position();
      var location = new TallComponents.Vector2(-1 * viewportPosition.x, -1 * viewportPosition.y);

      var viewportSize = this.get_minSize();
      return new TallComponents.Rectangle(location, viewportSize);
   },

   get_maxRectangle: function () {
      var viewportPosition = this.get_position();
      var location = new TallComponents.Vector2(-1 * viewportPosition.x, -1 * viewportPosition.y);

      var viewportSize = this.get_maxSize();
      return new TallComponents.Rectangle(location, viewportSize);
   },

   get_minRectangle: function () {
      var viewportPosition = this.get_position();
      var location = new TallComponents.Vector2(-1 * viewportPosition.x, -1 * viewportPosition.y);

      var viewportSize = this.get_minSize();
      return new TallComponents.Rectangle(location, viewportSize);
   },

   get_minSize: function () {
      ///<summary>Gets the min viewer size( with the scrollbars enabled).</summary>
      ///<remarks>The size is an object with the width and height properties set to the width and height in pixels.</remarks>

      if (this._min_viewsize == null) {
         //if the viewer size with the scrollbars enabled is still undefined, switch the scrollbars on and get the size

         //temorary save the overflow state
         var initialOverflowState = this._viewportElement.style.overflow;
         //enable scrollbars
         this._viewportElement.style.overflow = "scroll";
         //get size
         this._min_viewsize = new TallComponents.Size(this._viewportElement.clientWidth, this._viewportElement.clientHeight);
         //restore the initial overflow state
         this._viewportElement.style.overflow = initialOverflowState;
            }

      var minSize = this._min_viewsize.clone();
      return minSize;
   },

   get_maxSize: function () {
      ///<summary>Gets the max viewer size(without the scrollbars enabled).</summary>
      ///<remarks>The size is an object with the width and height properties set to the width and height in pixels.</remarks>

      var maxSize = new TallComponents.Size(parseInt(this._viewportElement.style.width), parseInt(this._viewportElement.style.height));
      return maxSize;
   },

   get_size: function () {
      ///<summary>Gets actual size of the viewport.</summary>
      var viewportMinSize = this.get_minSize();
      var viewportMaxSize = this.get_maxSize();

      var viewportWidth = this._scrollbars.get_isHorisontalEnabled() ? viewportMinSize.width : viewportMaxSize.width;
      var viewportHeight = this._scrollbars.get_isVerticalEnabled() ? viewportMinSize.height : viewportMaxSize.height;

      var size = new TallComponents.Size(viewportWidth, viewportHeight);

      return size;
   },

   set_size: function (size) {
      var changed = false;
      var oldSize = this.get_size();

      if (size.width > 0 && size.width != oldSize.width) {
         changed = true;
         this._viewportElement.style.width = size.width + 'px';
            }
      if (size.height > 0 && size.height != oldSize.height) {
         changed = true;
         this._viewportElement.style.height = size.height + 'px';
            }

      if (changed) {
         this._min_viewsize = null;
      }
      return changed;
   }
};

/////////////////////////  Layout settings object  ////////////////////

TallComponents.LayoutSettings = function (viewer) {
   this._viewer = viewer;
   this.initialize();
};

TallComponents.LayoutSettings.prototype = {

   initialize: function () {
   },

   get_verticalSpacing: function () {
      ///<summary>Gets a size of the vertical spacing between pages.</summary>
      return this._viewer.get_verticalSpacing();
   },

   get_horisontalSpacing: function () {
      ///<summary>Gets a size of the horisontal spacing between pages.</summary>
      return this._viewer.get_horisontalSpacing();
      }

};


/////////////////////////  Layout factory ////////////////////

TallComponents.LayoutFactory = function (document, layoutSettings) {
   this.initialize(layoutSettings, document);
};

TallComponents.LayoutFactory.prototype = {

   initialize: function (layoutSettings, document) {
      this._document = document;

      this._singleLayout = new TallComponents.LayoutSingle(layoutSettings, document);
      this._doubleLayout = new TallComponents.LayoutDouble(layoutSettings, document);
      this._continuousLayout = new TallComponents.LayoutContinuous(layoutSettings, document);
      this._continuousDoubleLayout = new TallComponents.LayoutContinuousDouble(layoutSettings, document);
      this._current = this._singleLayout;
   },

   set_document: function (value) {
      ///<summary>Sets a docuemnt which is to be used by a different layouts.</summary>
      this._document = value;
   },

   getByName: function (name) {
      ///<summary>Returns a layout object by its name.</summary>
      var pageIndex = this._current.get_currentPageIndex();
      switch (name) {
         case 'single':
            this._singleLayout.set_document(this._document);
            this._current = this._singleLayout;
            break;
         case 'double':
            this._doubleLayout.set_document(this._document);
            this._current = this._doubleLayout;
            break;
         case 'continuous':
            this._continuousLayout.set_document(this._document);
            this._current = this._continuousLayout;
            break;
         case 'continuousdouble':
            this._continuousDoubleLayout.set_document(this._document);
            this._current = this._continuousDoubleLayout;
            break;
         default:
            break;
      }
      this._current.set_currentPageIndex(pageIndex, true);
      return this._current;
   }
};

////////////////////// ContinuousDouble Layout object ////////////////////////////

TallComponents.LayoutContinuousDouble = function (layoutSettings, document) {
   this._document = document;
   this._layoutSettings = layoutSettings;
   this.initialize();
};

TallComponents.LayoutContinuousDouble.prototype = {

   initialize: function () {
      this._currentPageIndex = 0;
      this._lastVisiblePageIndex = 0;
      this._firstVisiblePageIndex = 0;
   },

   recalculatePageIndexes: function (zoom, viewportRect) {
      ///<summary>Recalculates indexes of the first, the last and the current page.</summary>
      if (isNaN(zoom) || zoom == 0){
         zoom = 1;
      }

      var zoomFactor = 1 / zoom;
      viewportRect.location.multiplyByNumber(zoomFactor);
      viewportRect.scale(zoomFactor);

      var top = viewportRect.location.y;
      var bottom = viewportRect.get_rightBottom().y;
      var verticalSpacing = this._layoutSettings.get_verticalSpacing();

      this._firstVisiblePageIndex = this._calculatePageIndex(this._document, top, verticalSpacing, true);
      this._lastVisiblePageIndex = this._calculatePageIndex(this._document, bottom, verticalSpacing, false);

      this._currentPageIndex = this._calculateCurrentPageIndex(this._document, verticalSpacing, viewportRect);
   },

   get_name: function () {
      ///<summary>Returns the name of the layout mode.</summary>
      return 'continuousdouble';
   },

   calculateSpreadSize: function () {
      ///<summary>Calculates the max size of a spread in the current document.</summary>
      var spreadSize = new TallComponents.Size(0, 0);
      var horisontalSpacing = this._layoutSettings.get_horisontalSpacing();
      if (this._document.IsValid) {
         //document width is a sum of the widest left page width, the widest right page width and a horisontal spacing.
         var even = this._calculateMaxPageWidth(this._document, false);
         var odd = this._calculateMaxPageWidth(this._document, true);
         spreadSize.width = even + odd + horisontalSpacing;
         spreadSize.height = this._calculateMaxPageHeight(this._document);
      }
      return spreadSize;
   },

   calculateContainerSize: function (zoom) {
      ///<summary>Calculates a size of the container.</summary>
      var size = this._calculateContentSize();
      size.width = (size.width * zoom).toFixed(0);
      size.height = (size.height * zoom).toFixed(0);
      return size;
   },

   calculatePagePositionX: function (pageIndex, zoom) {
      ///<summary>calculates the X offset of the first visible page.</summary>
      var page = this.getPageByIndex(pageIndex);
      var pageRect = this.getPageRectangle(page);
      var currentPageOffsetX = -1 * pageRect.location.x * zoom;
      return currentPageOffsetX;
   },

   calculatePagePositionY: function (pageIndex, zoom) {
      ///<summary>calculates the Y offset of the first visible page.</summary>
      var verticalSpacing = this._layoutSettings.get_verticalSpacing();
      var offset = 0;
      for (index = 1; ((index < pageIndex) && (index < this._document.Pages.length)); index += 2) {
         var leftPageSize = this._getPageSizeByPageIndex(index - 1);
         var rightPageSize = new TallComponents.Size(0, 0);

         if (this._document.Pages.length > index) {
            var rightPageSize = this._getPageSizeByPageIndex(index);
      }
         offset += Math.max(leftPageSize.height, rightPageSize.height);
         offset += verticalSpacing;
      }

      var currentPageOffsetY = -1 * offset * zoom;
      return currentPageOffsetY;
   },

   set_document: function (value) {
      ///<summary>Sets the document.</summary>
      this._document = value;
   },

   get_firstVisiblePageIndex: function () {
      ///<summary>Gets the index of the first visible page.</summary>
      return this._firstVisiblePageIndex;
   },

   get_lastVisiblePageIndex: function () {
      ///<summary>Gets the index of the last visible page.</summary>
      return this._lastVisiblePageIndex;
   },

   get_currentPageIndex: function () {
      ///<summary>Gets or sets the index of the current page.</summary>
      return this._currentPageIndex;
   },

   set_currentPageIndex: function (value, updateNeeded) {
      ///<summary>Gets or sets the index of the current page.</summary>
      this._currentPageIndex = value;
   },

   getPageOriginalSize: function (page) {
      ///<summary>Gets the size of the specified page.</summary>
      var pageSize = new TallComponents.Size(0, 0);

      if (page != null) {
         var rotation = page.rotation ? page.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
         pageSize.height = rotation % 180 !== 0 ? page.Width : page.Height;
      }
      return pageSize;
   },

   getPageRectangle: function (page) {
      var location = new TallComponents.Vector2(page.Left, page.Top);
      var size = this.getPageOriginalSize(page);

      var pageRectangle = new TallComponents.Rectangle(location, size);
      return pageRectangle;
   },

   getPageByIndex: function (pageIndex) {
      ///<summary>Gets a page with a specified index.</summary>
      var page = null;
      if (this._document && this._document.IsValid) {
         page = this._document.Pages[pageIndex];
      }
      return page;
   },

   calculateNextPageIndex: function (zoom, viewportRect) {
      ///<summary>Calculates the index of the next page.</summary>
      //get spread rectangle 
      var spreadRect = this._get_spreadRectangle(this._currentPageIndex, zoom);
      spreadRect.scale(zoom);

      var pageCount = this._document.Pages.length;
      var nextPageIndex = this._currentPageIndex;

      var pageOffset = (viewportRect.contains(spreadRect) && (this._currentPageIndex < pageCount - 2))? 2: 1 ;

      //apply the offset
      if (this._currentPageIndex < pageCount - pageOffset) {
         nextPageIndex = this._currentPageIndex + pageOffset;
      }
      
      return nextPageIndex;
   },

   calculatePreviousPageIndex: function (zoom, viewportRect) {
      ///<summary>Calculates the index of the previous page.</summary>
      //get spread rectangle 
      var spreadRect = this._get_spreadRectangle(this._currentPageIndex, zoom);
      spreadRect.scale(zoom);

      var pageCount = this._document.Pages.length;
      var previousPageIndex = this._currentPageIndex;

      var pageOffset = (viewportRect.contains(spreadRect) && (this._currentPageIndex - 2 >= 0))? 2: 1 ;

      //apply the offset
      if (this._currentPageIndex - pageOffset >= 0) {
         previousPageIndex = this._currentPageIndex - pageOffset;
      }

      return previousPageIndex;
   },

   rotate: function (degree) {
      ///<summary>Rotates pages.</summary>
      for (var pageIndex = 0; pageIndex < this._document.Pages.length; pageIndex++) {
         var page = this._getPage(pageIndex, this._document);
         if (page != null) {
            this._rotate(page, degree);
            }
         }
   },

   layout: function (document) {
      ///<summary>Performs layout for the set of the pages</summary>
      if (document.IsValid) {

         var verticalSpacing = this._layoutSettings.get_verticalSpacing();
         var horisontalSpacing = this._layoutSettings.get_horisontalSpacing();

         var y = 0; // y=0 means the upper edge of the image which will be generated
         var x = 0;

         var maxLeftWidth = this._calculateMaxPageWidth(document, false);
         var maxRightWidth = this._calculateMaxPageWidth(document, true);

         var lastPageIndex = this.get_lastVisiblePageIndex();

         for (var pageIndex = 0; pageIndex <= lastPageIndex; pageIndex += 2) {

            var firstPage = document.Pages[pageIndex];
            var page1Size = this._getPageSizeByPageIndex(pageIndex);

            firstPage.Top = y;
            firstPage.Left = maxLeftWidth - page1Size.width;

            var height = page1Size.height;

            if (pageIndex + 1 <= lastPageIndex) {
               var secondPage = document.Pages[pageIndex + 1];
               page2Size = this._getPageSizeByPageIndex(pageIndex + 1);

               height = Math.max(height, page2Size.height);

               firstPage.Top += (height - page1Size.height) / 2;
               secondPage.Top = y + (height - page2Size.height) / 2;
               secondPage.Left = firstPage.Left + page1Size.width + horisontalSpacing;
         }

            y += height + verticalSpacing;
      }
      }
   },

   _get_spreadRectangle: function(pageIndex, zoom) {
      var y = this.calculatePagePositionY(pageIndex, zoom);
      
      var spreadSize = this.calculateSpreadSize();
      var spreadLocation = new TallComponents.Vector2(0,-1*y);
      var spreadRect = new TallComponents.Rectangle(spreadLocation, spreadSize);

      return spreadRect;
   },

   _calculatePageIndex: function (document, boundPosition, verticalSpacing, isFirst) {
      var offset = 0;
      for (index = 0; (index < document.Pages.length); index += 2) {

         var leftPageSize = this._getPageSizeByPageIndex(index);
         var rightPageSize = { width: 0, height: 0 };

         if (document.Pages.length > index + 1) {
            rightPageSize = this._getPageSizeByPageIndex(index + 1);
         }

         offset += Math.max(leftPageSize.height, rightPageSize.height);
         if (index > 0) {
            offset += verticalSpacing;
         }

         if (offset > boundPosition) {
            if (isFirst) {
               return index;
            } else {
               if (document.Pages.length <= index + 1) {
                  return document.Pages.length - 1;
               }
               return index + 1;
            }
         }
      }
      // No valid page found, return last page
      return document.Pages.length - 1;
   },

   _calculateCurrentPageIndex: function (document, verticalSpacing, viewportRect) {
      var currentPositionY = 0;
      var maxHeight = 0;
      var resultPageIndex = 0;
      var isCurrentPageVisible = false;

      var leftTop = viewportRect.get_leftTop();
      var rightBottom = viewportRect.get_rightBottom();
      
      for (leftPageIndex = 0; leftPageIndex <= this._lastVisiblePageIndex; leftPageIndex += 2) {

         var rightPageIndex = leftPageIndex + 1;
         var rightPageExist = document.Pages.length > rightPageIndex;

         var leftPageSize = this._getPageSizeByPageIndex(leftPageIndex);
         var rightPageSize = (rightPageExist) ? this._getPageSizeByPageIndex(rightPageIndex) : new TallComponents.Size(0, 0);

         isCurrentPageVisible = this._currentPageIndex == leftPageIndex || this._currentPageIndex == rightPageIndex;

         var pageHeight = Math.max(leftPageSize.height, rightPageSize.height);

         if ((leftTop.y < currentPositionY + pageHeight) && (currentPositionY < rightBottom.y)) {

            var viewerHeight = viewportRect.size.height;
            var visiblePartHeight = Math.min(currentPositionY + pageHeight - leftTop.y, viewerHeight) - Math.max(currentPositionY - leftTop.y, 0);

            if (visiblePartHeight > maxHeight) {
               
               maxHeight = visiblePartHeight;

               //is there a page on the right side? 
               if (rightPageExist) {
                  //YES? Then it's time to decide which of two pages is visible - left or right?
                  
                  //get the width of the visible part of the left page
                  var leftPage = this.getPageByIndex(leftPageIndex);
                  var leftPageRect = this.getPageRectangle(leftPage);
                  var leftPageVisiblePart = leftPageRect.get_rightBottom().x - leftTop.x; 

                  //get the width of the visible part of the right page
                  var rightPage = this.getPageByIndex(rightPageIndex);
                  var rightPageRect = this.getPageRectangle(rightPage);
                  var rightPageVisiblePart = (rightBottom.x+20) - rightPageRect.location.x; 

                  var bothPagesFullyVisible = (rightPageRect.size.width < rightPageVisiblePart) && (leftPageRect.size.width < leftPageVisiblePart);

                  if (isCurrentPageVisible && bothPagesFullyVisible) {
                     resultPageIndex = this._currentPageIndex;
                  }
                  else {
                     resultPageIndex =  (rightPageVisiblePart > leftPageVisiblePart)? rightPageIndex : leftPageIndex;
                  }
               }
               else {
                  resultPageIndex = leftPageIndex;
               }

            }
         }

         currentPositionY += pageHeight;
         if (leftPageIndex > 0) {
            currentPositionY += verticalSpacing;
         }
      }
      return resultPageIndex;
   },

   _calculateContentSize: function () {
      ///<summary>Calculates document size</summary>
      var documentSize = new TallComponents.Size(0, 0);

      var verticalSpacing = this._layoutSettings.get_verticalSpacing();
      var horisontalSpacing = this._layoutSettings.get_horisontalSpacing();

      if (this._document.IsValid) {

         //document width is a sum of the widest left page width, the widest right page width and a horisontal spacing.
         var even = this._calculateMaxPageWidth(this._document, false);
         var odd = this._calculateMaxPageWidth(this._document, true);
         documentSize.width = even + odd + horisontalSpacing;
         //document height is a sum of pages height and a vertical spacing between them.
         for (pageIndex = 0; pageIndex < this._document.Pages.length; pageIndex += 2) {

            var leftPageSize = this._getPageSizeByPageIndex(pageIndex);
            var rightPageSize = new TallComponents.Size(0, 0);

            if (pageIndex + 1 < this._document.Pages.length) {
               rightPageSize = this._getPageSizeByPageIndex(pageIndex + 1);
         }

            documentSize.height += Math.max(leftPageSize.height, rightPageSize.height);

            if (pageIndex > 0) {
               documentSize.height += verticalSpacing;
         }
         }
      }
      return documentSize;
   },

   _calculateMaxPageWidth: function (document, oddPages) {
      //calculates the maximal odd/even pages width.
      var maxWidth = 0;
      var startIndex = (!oddPages) ? 0 : 1;
      for (var pageIndex = startIndex; pageIndex < document.Pages.length; pageIndex += 2) {

         var pageSize = this._getPageSizeByPageIndex(pageIndex);
         maxWidth = Math.max(maxWidth, pageSize.width);
      }
      return maxWidth;
   },

   _calculateMaxPageHeight: function (document) {
      //calculates the maximal pages height.
      var maxHeight = 0;

      for (var pageIndex = 0; pageIndex < document.Pages.length; pageIndex++) {

         var pageSize = this._getPageSizeByPageIndex(pageIndex);
         maxHeight = Math.max(maxHeight, pageSize.height);
      }
      return maxHeight;
   },

   _rotate: function (page, degree) {
      ///<summary>Rotates pages.</summary>
      var currentDegree = page.rotation ? page.rotation : 0;
      var newDegree = currentDegree + degree;

      var actualRotation = (newDegree % 360);
      if (actualRotation < 0) {
         actualRotation = 360 + actualRotation;
      }

      actualRotation = Math.round(actualRotation / 90) * 90;
      page.rotation = actualRotation;
   },

   _getPageSizeByPageIndex: function (pageIndex) {
      var pageSize = new TallComponents.Size(0, 0);
      var page = this._getPage(pageIndex, this._document);

      if (page != null) {
         pageSize = this.getPageOriginalSize(page);
      }
      return pageSize;
   },

   _getPage: function (pageIndex, document) {
      var page = null;

      if (document.IsValid && pageIndex >= 0 && pageIndex < document.Pages.length) {
         page = document.Pages[pageIndex];
      }
      return page;
   }
};



///////////////////// Continuous Layout object ///////////////

TallComponents.LayoutContinuous = function (layoutSettings, document) {
   this._document = document;
   this._layoutSettings = layoutSettings;
   this.initialize();
};

TallComponents.LayoutContinuous.prototype = {

   initialize: function () {
      this._currentPageIndex = 0;
      this._lastVisiblePageIndex = 0;
      this._firstVisiblePageIndex = 0;
   },

   recalculatePageIndexes: function (zoom, viewportRect) {
      ///<summary>Recalculates indexes of the first, the last and the current page.</summary>
      if (isNaN(zoom) || zoom == 0){
         zoom = 1;
      }

      var zoomFactor = 1 / zoom;
      viewportRect.location.multiplyByNumber(zoomFactor);
      viewportRect.scale(zoomFactor);

      var verticalSpacing = this._layoutSettings.get_verticalSpacing();

      var top = viewportRect.location.y;
      this._firstVisiblePageIndex = this._calculatePageIndex(this._document, top, verticalSpacing, 0);

      var bottom = viewportRect.get_rightBottom().y;
      this._lastVisiblePageIndex = this._calculatePageIndex(this._document, bottom, 0, verticalSpacing);

      this._currentPageIndex = this._calculateCurrentPageIndex(this._document, top, bottom, verticalSpacing, viewportRect.size, zoom);
   },

   get_name: function () {
      ///<summary>Returns the name of the layout mode.</summary>
      return 'continuous';
   },

   calculateSpreadSize: function () {
      ///<summary>Calculates the max size of a spread in the current document.</summary>
      var spreadSize = new TallComponents.Size(0, 0);
      if (this._document.IsValid) {
         for (pageIndex = 0; pageIndex < this._document.Pages.length; pageIndex++) {
            var size = this._getPageSizeByPageIndex(pageIndex);
            spreadSize.width = Math.max(spreadSize.width, size.width);
            spreadSize.height = Math.max(spreadSize.height, size.height);
         }
      }
      return spreadSize;
   },

   calculateContainerSize: function (zoom) {
      ///<summary>Calculates a size of the container.</summary>
      var size = this._calculateContentSize();
      size.width = (size.width * zoom).toFixed(0);
      size.height = (size.height * zoom).toFixed(0);
      return size;
   },

   calculatePagePositionX: function (pageIndex, zoom) {
      ///<summary>calculates the X offset of the first visible page.</summary>
      var page = this.getPageByIndex(pageIndex);
      var pageRect = this.getPageRectangle(page);
      var currentPageOffsetX = -1 * pageRect.location.x * zoom;
      return currentPageOffsetX;
   },

   calculatePagePositionY: function (pageIndex, zoom) {
      ///<summary>calculates the Y offset of the first visible page.</summary>
      var verticalSpacing = this._layoutSettings.get_verticalSpacing();

      var offset = 0;
      var pageSize = null;
      for (index = 0; ((index < pageIndex) && (index < this._document.Pages.length)); index++) {
         pageSize = this._getPageSizeByPageIndex(index);
         offset += pageSize.height + verticalSpacing;
      }
      var currentPageOffsetY = ((-1 * offset) * zoom);
      return currentPageOffsetY;
   },

   set_document: function (value) {
      ///<summary>Sets the document.</summary>
      this._document = value;
   },

   get_firstVisiblePageIndex: function () {
      ///<summary>Gets the index of the first visible page.</summary>
      return this._firstVisiblePageIndex;
   },

   get_lastVisiblePageIndex: function () {
      ///<summary>Gets the index of the last visible page.</summary>
      return this._lastVisiblePageIndex;
   },

   get_currentPageIndex: function () {
      ///<summary>Gets or sets the index of the current page.</summary>
      return this._currentPageIndex;
   },

   set_currentPageIndex: function (value) {
      ///<summary>Gets or sets the index of the current page.</summary>
      this._currentPageIndex = value;
   },

   getPageOriginalSize: function (page) {
      ///<summary>Gets the size of the specified page.</summary>
      var pageSize = new TallComponents.Size(0,0);

      if (page != null) {
         var rotation = page.rotation ? page.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
         pageSize.height = rotation % 180 !== 0 ? page.Width : page.Height;
      }
      return pageSize;
   },

   getPageRectangle: function (page) {
      var location = new TallComponents.Vector2(page.Left, page.Top);
      var size = this.getPageOriginalSize(page);

      var pageRectangle = new TallComponents.Rectangle(location, size);
      return pageRectangle;
   },

   getPageByIndex: function (pageIndex) {
      ///<summary>Gets a page with a specified index.</summary>
      var page = null;
      if (this._document && this._document.IsValid) {
         page = this._document.Pages[pageIndex];
      }
      return page;
   },

   calculateNextPageIndex: function () {
      ///<summary>Calculates the index of the next page.</summary>
      var pageCount = this._document.Pages.length;

      var nextPageIndex = this._currentPageIndex;

      if (this._currentPageIndex < pageCount - 1) {
         nextPageIndex = this._currentPageIndex + 1;
         }
      return nextPageIndex;
   },

   calculatePreviousPageIndex: function () {
      ///<summary>Calculates the index of the previous page.</summary>
      var prevPageIndex = this._currentPageIndex;
      if (this._currentPageIndex > 0) {
         prevPageIndex = this._currentPageIndex - 1;
      }
      return prevPageIndex;
   },

   rotate: function (degree) {
      ///<summary>Rotates pages.</summary>
      for (var pageIndex = 0; pageIndex < this._document.Pages.length; pageIndex++) {
         var page = this._getPage(pageIndex, this._document);
         if (page != null) {
            this._rotate(page, degree);
         }
      }
   },

   layout: function (document) {
         ///<summary>Performs layout for the set of the pages</summary>
      if (document.IsValid) {
         var y = 0; // y=0 means the upper edge of the image which will be generated
         var x = 0;

         var verticalSpacing = this._layoutSettings.get_verticalSpacing();
         var maxWidth = 0;

         for (var pageIndex = 0; pageIndex < document.Pages.length; pageIndex++) {
            var page = this._getPage(pageIndex, document);
            var pageSize = this.getPageOriginalSize(page);
            maxWidth = Math.max(maxWidth, pageSize.width);
         }

         var lastPageIndex = this.get_lastVisiblePageIndex(0);
         for (pageIndex = 0; pageIndex <= lastPageIndex; pageIndex++) {

            var page = this._getPage(pageIndex, document);
            pageSize = this.getPageOriginalSize(page);
            page.Top = y;
            page.Left = (maxWidth - pageSize.width) / 2;
            y += pageSize.height + verticalSpacing;
         }
      }
   },

   _calculateCurrentPageIndex: function (document, top, bottom, verticalSpacing, viewerSize, zoom) {
      var offset = 0;
      var maxHeight = 0;
      var maxPageIndex = 0;

      for (index = 0; index <= this._lastVisiblePageIndex; index++) {
         var pageSize = this._getPageSizeByPageIndex(index);

         if ((offset + pageSize.height > top) && (offset < bottom)) {
            var viewerHeight = (viewerSize.height / zoom).toFixed(0);
            var visiblePartHeight = Math.min(offset + pageSize.height - top, viewerHeight) - Math.max(offset - top, 0);

            if (visiblePartHeight > maxHeight) {
               maxHeight = visiblePartHeight;
               maxPageIndex = index;
            }
         }

         offset += pageSize.height;
         if (index > 0) {
            offset += verticalSpacing;
         }
      }
      return maxPageIndex;
   },

   _calculateContentSize: function () {
      ///<summary>Calculates document size</summary>
      var documentSize = new TallComponents.Size(0, 0);
      var verticalSpacing = this._layoutSettings.get_verticalSpacing();

      if (this._document.IsValid) {
         for (pageIndex = 0; pageIndex < this._document.Pages.length; pageIndex++) {
            var size = this._getPageSizeByPageIndex(pageIndex);
            documentSize.width = Math.max(documentSize.width, size.width);
            documentSize.height += size.height;
            if (pageIndex > 0) {
               documentSize.height += verticalSpacing;
            }
         }
      }
      return documentSize;
   },

   _rotate: function (page, degree) {
      ///<summary>Rotates pages.</summary>
      var currentDegree = page.rotation ? page.rotation : 0;
      var newDegree = currentDegree + degree;

      var actualRotation = (newDegree % 360);
      if (actualRotation < 0) {
         actualRotation = 360 + actualRotation;
      }

      actualRotation = Math.round(actualRotation / 90) * 90;
      page.rotation = actualRotation;
   },

   _calculatePageIndex: function (document, borderPositionY, additionalSpaceBeforePage, additionalSpaceAfterPage) {
      if (document.IsValid) {
         var offset = 0;
         for (index = 0; (index < document.Pages.length); index++) {
            var pageSize = this._getPageSizeByPageIndex(index);
            offset += pageSize.height;

            if (index > 0) {
               offset += additionalSpaceBeforePage;
            }
            if (offset >= borderPositionY) {
               return index;
            }
            if (index > 0) {
               offset += additionalSpaceAfterPage;
            }
         }
         return document.Pages.length - 1;
      }
      return 0;
   },

   _getPageSizeByPageIndex: function (pageIndex) {
      var pageSize = { width: 0, height: 0 };
      var page = this._getPage(pageIndex, this._document);

      if (page != null) {
         pageSize = this.getPageOriginalSize(page);
      }
      return pageSize;
   },

   _getPage: function (pageIndex, document) {
      var page = null;

      if (document.IsValid && pageIndex >= 0 && pageIndex < document.Pages.length) {
         page = document.Pages[pageIndex];
      }
      return page;
   }
};


///////////////////// Double Layout object ///////////////

TallComponents.LayoutDouble = function (layoutSettings, document) {
   this._document = document;
   this._layoutSettings = layoutSettings;
   this.initialize();
};

TallComponents.LayoutDouble.prototype = {

   initialize: function () {
      this._currentPageIndex = 0;
   },

   recalculatePageIndexes: function (zoom, viewportRect) {
      ///<summary>Recalculates indexes of the first, the last and the current page.</summary>
      
      if (isNaN(zoom) || zoom == 0){
         zoom = 1;
      }
      
      var zoomFactor = 1 / zoom;
      viewportRect.location.multiplyByNumber(zoomFactor);
      viewportRect.scale(zoomFactor);

      var leftTop = viewportRect.get_leftTop();
      var rightBottom = viewportRect.get_rightBottom();
      
      var verticalSpacing = this._layoutSettings.get_verticalSpacing();
      
      var leftPageIndex = this.get_firstVisiblePageIndex();
      var rightPageIndex = leftPageIndex + 1;
      
      var rightPageExist = this._document.Pages.length > rightPageIndex;
      var resultPageIndex = leftPageIndex;
      
      //is there a page on the right side? 
      if (rightPageExist) {
         //YES? Then it's time to decide which of two pages is visible - left or right?
         
         //get the width of the visible part of the left page
         var leftPage = this.getPageByIndex(leftPageIndex);
         var leftPageRect = this.getPageRectangle(leftPage);
         var leftPageVisiblePart = leftPageRect.get_rightBottom().x - leftTop.x; 
         
         //get the width of the visible part of the right page
         var rightPage = this.getPageByIndex(rightPageIndex);
         var rightPageRect = this.getPageRectangle(rightPage);
         var rightPageVisiblePart = (rightBottom.x+20) - rightPageRect.location.x; 
         
         var bothPagesFullyVisible = (rightPageRect.size.width < rightPageVisiblePart) && (leftPageRect.size.width < leftPageVisiblePart);
         
         if (bothPagesFullyVisible) {
            resultPageIndex = this._currentPageIndex;
         }
         else {
            resultPageIndex =  (rightPageVisiblePart > leftPageVisiblePart)? rightPageIndex : leftPageIndex;
         }
      }
      else {
         resultPageIndex = leftPageIndex;
      }

      this._currentPageIndex = resultPageIndex;
   },

   get_name: function () {
      ///<summary>Returns the name of the layout mode.</summary>
      return 'double';
   },

   calculateSpreadSize: function () {
      ///<summary>Calculates the max size of a spread in the current document.</summary>
      var pageIndex = this.get_currentPageIndex();

      var spreadSize = new TallComponents.Size(0, 0);
      var spacing = this._layoutSettings.get_horisontalSpacing();

      var leftPage = this._getLeftPage(pageIndex, this._document);
      var leftPageSize = this.getPageOriginalSize(leftPage);

      var rightPage = this._getRightPage(pageIndex, this._document);
      var rightPageSize = this.getPageOriginalSize(rightPage);

      spreadSize.width = (rightPageSize.width > 0) ? leftPageSize.width + spacing + rightPageSize.width : leftPageSize.width;
      spreadSize.height = (rightPageSize.height > 0) ? Math.max(leftPageSize.height, rightPageSize.height) : leftPageSize.height;

      return spreadSize;
   },

   calculateContainerSize: function (zoom) {
      ///<summary>Calculates a size of the container.</summary>
      var size = this._calculateContentSize();
      size.width = (size.width * zoom).toFixed(0);
      size.height = (size.height * zoom).toFixed(0);
      return size;
   },

   calculatePagePositionX: function (pageIndex, zoom) {
      ///<summary>calculates the X offset of the first visible page.</summary>
      
      var page = this.getPageByIndex(pageIndex);
      var pageRect = this.getPageRectangle(page);
      var currentPageOffsetX = -1 * pageRect.location.x * zoom;

      return currentPageOffsetX;
   },

   calculatePagePositionY: function (pageIndex) {
      ///<summary>calculates the Y offset of the first visible page.</summary>
      return 0;
   },

   set_document: function (value) {
      ///<summary>Sets the document.</summary>
      this._document = value;
   },

   get_firstVisiblePageIndex: function () {
      ///<summary>Gets the index of the first visible page.</summary>
      var pageIndex = this.get_currentPageIndex();
      return this._calculateLeftPageIndex(pageIndex);
   },

   get_lastVisiblePageIndex: function () {
      ///<summary>Gets the index of the last visible page.</summary>
      var pageIndex = this.get_currentPageIndex();

      var rightPageIndex = this._calculateRightPageIndex(pageIndex);

      if (this._document.IsValid && this._document.Pages.length <= rightPageIndex) {
         rightPageIndex = this.get_firstVisiblePageIndex();
            }
      return rightPageIndex;
   },

   get_currentPageIndex: function () {
      ///<summary>Gets or sets the index of the current page.</summary>
      return this._currentPageIndex;
   },

   set_currentPageIndex: function (value) {
      ///<summary>Gets or sets the index of the current page.</summary>
      this._currentPageIndex = value;
   },

   getPageOriginalSize: function (page) {
      ///<summary>Gets the size of the specified page.</summary>
      var pageSize = new TallComponents.Size(0, 0);

      if (page != null) {
         var rotation = page.rotation ? page.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
         pageSize.height = rotation % 180 !== 0 ? page.Width : page.Height;
      }
      return pageSize;
   },

   getPageRectangle: function (page) {
      var location = new TallComponents.Vector2(page.Left, page.Top);
      var size = this.getPageOriginalSize(page);

      var pageRectangle = new TallComponents.Rectangle(location, size);
      return pageRectangle;
   },

   getPageByIndex: function (pageIndex) {
      ///<summary>Gets a page with a specified index.</summary>
      var page = null;
      if (this._document && this._document.IsValid) {
         page = this._document.Pages[pageIndex];
         }
      return page;
   },

   calculateNextPageIndex: function (zoom, viewportRect) {
      ///<summary>Calculates the index of the next page.</summary>

      //get spread rectangle 
      var spreadRect = this._get_spreadRectangle();
      spreadRect.scale(zoom);

      var pageCount = this._document.Pages.length;
      var nextPageIndex = this._currentPageIndex;

      var pageOffset = (viewportRect.contains(spreadRect) && (this._currentPageIndex < pageCount - 2))? 2: 1 ;

      //apply the offset
      if (this._currentPageIndex < pageCount - pageOffset) {
         nextPageIndex = this._currentPageIndex + pageOffset;
      }
      
      return nextPageIndex;
   },

   calculatePreviousPageIndex: function (zoom, viewportRect) {
      ///<summary>Calculates the index of the previous page.</summary>

      //get spread rectangle 
      var spreadRect = this._get_spreadRectangle();
      spreadRect.scale(zoom);

      var pageCount = this._document.Pages.length;
      var previousPageIndex = this._currentPageIndex;

      var pageOffset = (viewportRect.contains(spreadRect) && (this._currentPageIndex - 2 >= 0))? 2: 1 ;

      //apply the offset
      if (this._currentPageIndex - pageOffset >= 0) {
         previousPageIndex = this._currentPageIndex - pageOffset;
      }

      return previousPageIndex;
   },

   _get_spreadRectangle: function() {
      var spreadSize = this.calculateSpreadSize();
      var spreadLocation = new TallComponents.Vector2(0,0);
      var spreadRect = new TallComponents.Rectangle(spreadLocation, spreadSize);
      return spreadRect;
   },

   rotate: function (degree) {
      ///<summary>Rotates pages.</summary>
      var pageIndex = this._currentPageIndex;

      var leftPage = this._getLeftPage(pageIndex, this._document);
      if (leftPage != null) {
         this._rotate(leftPage, degree);
      }

      var rightPage = this._getRightPage(pageIndex, this._document);
      if (rightPage != null) {
         this._rotate(rightPage, degree);
      }
   },

   layout: function (document) {
      ///<summary>Performs layout for the set of the pages</summary>
      if (document.IsValid) {
         var y = 0; // y=0 means the upper edge of the image which will be generated
         var x = 0;

         var pageIndex = this._currentPageIndex;

         var leftPage = this._getLeftPage(pageIndex, document);
         if (leftPage != null) {
            leftPage.Top = y;
            leftPage.Left = x;
      }

         var rightPage = this._getRightPage(pageIndex, document);
         if (rightPage != null) {
            var leftPageSize = this.getPageOriginalSize(leftPage);
            var rightPageSize = this.getPageOriginalSize(rightPage);

            rightPage.Left = x + leftPageSize.width + this._layoutSettings.get_horisontalSpacing();
            rightPage.Top = y;

            //align pages
            var maxHeight = Math.max(leftPageSize.height, rightPageSize.height);

            leftPage.Top += (maxHeight - leftPageSize.height) / 2;
            rightPage.Top += (maxHeight - rightPageSize.height) / 2;
         }
      }
   },

   _calculateContentSize: function () {
      ///<summary>Calculates document size in Double Layout</summary>
      var pageSize = new TallComponents.Size(0, 0);
      var pageIndex = this.get_currentPageIndex();

      var leftPage = this._getLeftPage(pageIndex, this._document);

      if (leftPage != null) {
         var rotation = leftPage.rotation ? leftPage.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? leftPage.Height : leftPage.Width;
         pageSize.height = rotation % 180 !== 0 ? leftPage.Width : leftPage.Height;
      }

      var rightPage = this._getRightPage(pageIndex, this._document);

      if (rightPage != null) {
         var rotation = rightPage.rotation ? rightPage.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? pageSize.width + rightPage.Height : pageSize.width + rightPage.Width;
         pageSize.width += this._layoutSettings.get_horisontalSpacing();
         pageSize.height = Math.max(pageSize.height, rotation % 180 !== 0 ? rightPage.Width : rightPage.Height);
      }
      return pageSize;
   },

   _rotate: function (page, degree) {
      var currentDegree = page.rotation ? page.rotation : 0;
      var newDegree = currentDegree + degree;

      var actualRotation = (newDegree % 360);
      if (actualRotation < 0) {
         actualRotation = 360 + actualRotation;
      }

      actualRotation = Math.round(actualRotation / 90) * 90;
      page.rotation = actualRotation;
   },


   _calculateLeftPageIndex: function (pageIndex) {
      var leftPageIndex = pageIndex % 2 === 0 ? pageIndex : pageIndex - 1;
      return leftPageIndex;
   },

   _calculateRightPageIndex: function (pageIndex) {
      var rightPageIndex = pageIndex % 2 === 0 ? pageIndex + 1 : pageIndex;
      return rightPageIndex;
   },

   _getLeftPage: function (pageIndex, document) {
      var leftPage = null;
      var leftPageIndex = this._calculateLeftPageIndex(pageIndex);

      if (document.IsValid && leftPageIndex >= 0 && leftPageIndex < document.Pages.length) {
         leftPage = document.Pages[leftPageIndex];
            }
      return leftPage;
   },

   _getRightPage: function (pageIndex, document) {
      var rightPage = null;
      var rightPageIndex = this._calculateRightPageIndex(pageIndex);

      if (this._document.IsValid && rightPageIndex >= 0 && rightPageIndex < this._document.Pages.length) {
         rightPage = this._document.Pages[rightPageIndex];
            }
      return rightPage;
      }
};


///////////////////// Single Layout object ///////////////

TallComponents.LayoutSingle = function (layoutSettings, document) {
   this._document = document;
   this._layoutSettings = layoutSettings;
   this.initialize();
};

TallComponents.LayoutSingle.prototype = {

   initialize: function () {
      this._currentPageIndex = 0;
   },

   recalculatePageIndexes: function (zoom, viewportRect) {
      ///<summary>Recalculates indexes of the first, the last and the current page.</summary>
   },

   get_name: function () {
      ///<summary>Returns a name of the layout.</summary>
      return 'single';
   },

   calculateSpreadSize: function () {
      ///<summary>Calculates the max size of a spread in the current document.</summary>
      var spreadSize = new TallComponents.Size(0, 0);
      spreadSize.rotation = 0;
      var pageIndex = this.get_currentPageIndex();

      if (!this._document.IsValid || pageIndex < 0 || pageIndex >= this._document.Pages.length) {
         return spreadSize;
      }

      var page = this._document.Pages[pageIndex];
      var rotation = page.rotation ? page.rotation : 0;

      spreadSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
      spreadSize.height = rotation % 180 !== 0 ? page.Width : page.Height;

      return spreadSize;
   },

   calculateContainerSize: function (zoom) {
      ///<summary>Calculates a size of the container.</summary>
      var size = this._calculateContentSize();
      size.width = (size.width * zoom).toFixed(0);
      size.height = (size.height * zoom).toFixed(0);
      return size;
   },

   calculatePagePositionX: function (pageIndex) {
      ///<summary>calculates the X offset of the first visible page.</summary>
      return 0;
   },

   calculatePagePositionY: function (pageIndex) {
      ///<summary>calculates the Y offset of the first visible page.</summary>
      return 0;
   },

   set_document: function (value) {
      ///<summary>Sets the document.</summary>
      this._document = value;
   },

   get_firstVisiblePageIndex: function () {
      ///<summary>Gets the index of the first visible page.</summary>
      return this.get_currentPageIndex();
   },

   get_lastVisiblePageIndex: function () {
      ///<summary>Gets the index of the last visible page.</summary>
      return this.get_currentPageIndex();
   },

   get_currentPageIndex: function () {
      ///<summary>Gets or sets the index of the current page.</summary>
      return this._currentPageIndex;
   },

   set_currentPageIndex: function (value) {
      ///<summary>Gets or sets the index of the current page.</summary>
      this._currentPageIndex = value;
   },

   calculateNextPageIndex: function () {
      ///<summary>Calculates the index of the next page.</summary>
      var nextPageIndex = this._currentPageIndex;

      if (this._currentPageIndex < this._document.Pages.length - 1) {
         nextPageIndex = this._currentPageIndex + 1;
         }
      return nextPageIndex;
   },

   calculatePreviousPageIndex: function () {
      ///<summary>Calculates the index of the previous page.</summary>
      var prevPageIndex = this._currentPageIndex;
      if (prevPageIndex > 0) {
         prevPageIndex = this._currentPageIndex - 1;
      }
      return prevPageIndex;
   },

   getPageByIndex: function (pageIndex) {
      ///<summary>Gets a page with a specified index.</summary>
      var page = null;
      if (this._document && this._document.IsValid) {
         page = this._document.Pages[pageIndex];
      }
      return page;
   },

   getPageOriginalSize: function (page) {
      ///<summary>Gets the size of the specified page.</summary>
      var pageSize = new TallComponents.Size(0,0);

      if (page != null) {
         var rotation = page.rotation ? page.rotation : 0;

         pageSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
         pageSize.height = rotation % 180 !== 0 ? page.Width : page.Height;
      }
      return pageSize;
   },

   getPageRectangle: function (page) {
      var location = new TallComponents.Vector2(page.Left, page.Top);
      var size = this.getPageOriginalSize(page);

      var pageRectangle = new TallComponents.Rectangle(location, size);
      return pageRectangle;
   },

   rotate: function (degree) {
      ///<summary>Rotates pages.</summary>

      var pageIndex = this._currentPageIndex;

      var currentDegree = this._document.Pages[pageIndex].rotation ? this._document.Pages[pageIndex].rotation : 0;
      var newDegree = currentDegree + degree;

      var actualRotation = (newDegree % 360);
      if (actualRotation < 0) {
         actualRotation = 360 + actualRotation;
            }
      actualRotation = Math.round(actualRotation / 90) * 90;
      this._document.Pages[pageIndex].rotation = actualRotation;
   },

   layout: function (document) {
      //<summary>Performs layout for the one single page</summary>
      var pageIndex = this._currentPageIndex;

      if (document.IsValid) {
         var y = 0; // y=0 means the upper edge of the image which will be generated
         var x = 0;

         var page = document.Pages[pageIndex];
         page.Top = y;
         page.Left = x;
            }
   },

   _calculateContentSize: function () {
      var pageSize = new TallComponents.Size(0, 0);
pageSize.rotation = 0;
      var pageIndex = this.get_currentPageIndex();

      var isPageIndexWithinBounds = pageIndex >= 0 && pageIndex < this._document.Pages.length;

      if (this._document.IsValid && isPageIndexWithinBounds) {
            var page = this._document.Pages[pageIndex];
            var rotation = page.rotation ? page.rotation : 0;
         pageSize.rotation = rotation;
         pageSize.width = rotation % 180 !== 0 ? page.Height : page.Width;
         pageSize.height = rotation % 180 !== 0 ? page.Width : page.Height;
            }
      return pageSize;
         }
};

///////////////////End Single Layout ////////////////////

///////////////////////// Document Container object /////////////////////

TallComponents.DocumentContainer = function (element) {

   this._container = element;
   this._size = new TallComponents.Size(100,100);
};

TallComponents.DocumentContainer.prototype = {

   initialize: function () {
   },

   set_size: function (size) {
      this._size = size;
      this._container.style.width = this._size.width + "px";
      this._container.style.height = this._size.height + "px";
   },

   get_size: function () {
      return this._size;
   },

   addRectangle: function (rectangle, cssClass, id, javascript, styleProps) {
      if (rectangle) {
         var css = cssClass ? String.format('class="{0}" ', cssClass) : '';
         var js = javascript ? javascript : '';
         var style = styleProps ? styleProps : '';
         var idAttr = id ? String.format('id="{0}" ', id) : '';
         var selection = String.format('<div {0}{1}style="position:absolute;z-index:1;top:{2}px;left:{3}px;width:{4}px;height:{5}px;{7}" {6}></div>',
                                            idAttr,
                                            css,
                                            rectangle.location.y,
                                            rectangle.location.x,
                                            rectangle.size.width,
                                            rectangle.size.height,
                                            js,
                                            style);
         this._container.innerHTML += selection;
      }
   },

   removeRectangles: function (idStartWith) {
      var divs = this._container.getElementsByTagName('div');
      for (var i = divs.length - 1; i >= 0; i--) {
         if (divs[i].id && divs[i].id.startsWith(idStartWith)) {
            this._container.removeChild(divs[i]);
         }
      }
   },

   setImageSize: function (imageUrl, size) {
      this._container.innerHTML = '<img id="clientzoom" src="' + imageUrl + '" style="width:' + size.width + 'px;height:' + size.height + 'px' + '" onmousedown="return false;" />';
   },

   setImage: function (imageUrl, position) {
      this._container.style.backgroundRepeat = "no-repeat";
      this._container.style.backgroundPosition = (-1 * position.x) + "px " + (-1 * position.y) + "px";
      this._container.style.backgroundImage = "url(" + imageUrl + ")";
      this._container.innerHTML = ' ';
   },

   removeImage: function () {
      this._container.style.backgroundImage = '';
   },

   getImageUrl: function () {
      return this._container.style.backgroundImage;
   }

};

///////////////////////// End Document Container object /////////////////////

///////////////////////// Link Provider object /////////////////////

TallComponents.LinkProvider = function (element, viewerId) {
   this._container = element;
   this._viewerId = viewerId;
};

TallComponents.LinkProvider.prototype = {

   initialize: function () {
   },

   setupLinksOnVisiblePages: function (documentContainer, layout, zoom) {
      ///<summary>Sets up links on the currently visible pages.</summary>
      ///<remarks>Takes an information about the page rotation and zoom factor into account.</remarks>
      var firstPageIndex = layout.get_firstVisiblePageIndex();
      var lastPageIndex = layout.get_lastVisiblePageIndex();

      for (var pageIndex = firstPageIndex; pageIndex <= lastPageIndex; pageIndex++) {
         var page = layout.getPageByIndex(pageIndex);

         if (!(page && page.Links)) {
            //go to the next page if the page is null or it doesn't have links
            continue;
         }
         var pageRectangle = layout.getPageRectangle(page);

         //put each link at the right position
         for (var linkIndex = 0; linkIndex < page.Links.length; linkIndex++) {
            //get the information about the link area
            var lr = page.Links[linkIndex];
            var linkRectangle = new TallComponents.Rectangle(new TallComponents.Vector2(lr.Left, lr.Top), new TallComponents.Size(lr.Width, lr.Height));
            //rotate the selection if the page is rotated
            linkRectangle.rotateToParent(page.rotation, pageRectangle);
            //selection rectangle has the coordinates with respect to the page coordinate system,
            //here we convert the coordinates to the container coordinate system
            linkRectangle.translateToGlobal(pageRectangle);
            //modify size and location of the selection with respect to current zoom factor.
            linkRectangle.scale(zoom);
            linkRectangle.location.multiplyByNumber(zoom);

            var js = String.format('onclick="$find(\u0027{0}\u0027)._link({1},{2});"', this._viewerId, pageIndex, linkIndex);
            //put the selection on the container
            documentContainer.addRectangle(linkRectangle, null, null, js, ';cursor:pointer');
         }
     }
   }
};

///////////////////////// End Link Provider object /////////////////////


TallComponents.PdfViewer = function (element) {
   /// <summary>
   /// Displays PDF content and allows panning, zooming and browsing.
   /// </summary>
   TallComponents.PdfViewer.initializeBase(this, [element]);

   this._selectionIds = '__sel';

   this._documentContainer = null;
   this._background = null;
   this._serviceUrl;
   this._imgPreload = null;
   this._document = null;

   this._selection = null;
   this._updateSelection = false;
   this._search = { startPageIndex: null, index: null, results: null, query: null, lookedPagesCount: 0 };
   this._searchResultCssClass = 'searchResultSelectionRectangle_guid12345';
   this._searchServiceUrl;

   var elt = this.get_element();
   this._cursor = new TallComponents.Cursor(elt);
   this._image = new TallComponents.Image(elt);

   this.initialize();
};

//
// Create the prototype for the control.
//
TallComponents.PdfViewer.prototype = {

   initialize: function () {
      ///<summary>Initializes the control.</summary>
      ///<remarks>The initialize method initializes the control and sets the base <see cref="M:J#Sys.Component.isInitialized"/> property to true.</remarks>
      TallComponents.PdfViewer.callBaseMethod(this, "initialize");

      this._suppressOnScrollEvent = false;
      var element = this.get_element();
      //need to be removed
      this._container = element;

      this._documentContainer = new TallComponents.DocumentContainer(element);
      this._scrollbars = new TallComponents.Scrollbars(element);
      this._viewport = new TallComponents.Viewport(element, this._scrollbars, this._documentContainer);
      this._zoom = new TallComponents.Zoom(element, this._viewport);
      this._background = new TallComponents.Background(element);

      this._layoutSettings = new TallComponents.LayoutSettings(this);
      this._layoutFactory = new TallComponents.LayoutFactory(this._document, this._layoutSettings);
      this._currentLayout = this._layoutFactory.getByName('single');
      this._linksProvider = new TallComponents.LinkProvider(element, this.get_id());

      this._backSearch = true;

      $addHandlers(this._container, { mousedown: this._onMouseDown, mouseup: this._onMouseUp, mousemove: this._onMouseMove }, this);
      $addHandlers(document, { mouseup: this._endDrag }, this);
      $addHandlers(this._container.parentNode, { scroll: this._onScroll }, this);
      this._fetchNewImage('initialize');
   },

   dispose: function () {
      ///<summary>Removes the current control from the application.</summary>
      ///<remarks>The dispose method releases all resources from the <see cref="T:J#Sys.UI.Control"/> object, 
      ///unbinds it from its associated HTML Document Object Model (DOM) element, 
      ///and unregisters it from the application.</remarks>
      //      $clearHandlers(this._container);
      $clearHandlers(this._container.parentNode);
      //      $clearHandlers(document);

      if (null !== this._imgPreload) {
         $clearHandlers(this._imgPreload);
         this._imgPreload = null;
      }

      TallComponents.PdfViewer.callBaseMethod(this, 'dispose');
   },

   goTo: function (pageIndex, zoom, position) {
      if (isNaN(zoom) || (zoom == null)) {
         var spreadSize = this._currentLayout.calculateSpreadSize();
         zoom = this._zoom.calculateZoomValue(spreadSize);
      }

      this.set_pageIndex(pageIndex);
      if (position) {

         if ((!isNaN(position.x)) && this._scrollbars.get_isHorisontalEnabled()) {
            this._scrollbars.set_positionX(-1 * position.x * zoom);
         }

         if ((!isNaN(position.y)) && this._scrollbars.get_isVerticalEnabled()) {
            this._scrollbars.set_positionY(-1 * position.y * zoom);
         }
      }
   },

   nextPage: function () {
      var spreadSize = this._currentLayout.calculateSpreadSize();
      var zoom = this._zoom.calculateZoomValue(spreadSize);

      var viewportRect = this._viewport.get_maxRectangle();

      var newPageIndex = this._currentLayout.calculateNextPageIndex(zoom, viewportRect);
      this._internalSetPageIndex(newPageIndex, true);
   },

   previousPage: function () {
      var spreadSize = this._currentLayout.calculateSpreadSize();
      var zoom = this._zoom.calculateZoomValue(spreadSize);

      //get viewport rectangle
      var viewportRect = this._viewport.get_maxRectangle();

      var newPageIndex = this._currentLayout.calculatePreviousPageIndex(zoom, viewportRect);
      this._internalSetPageIndex(newPageIndex, true);
   },

   rotate: function (rotation) {
      ///<summary>Performs rotation. The result depends on current page layout (whether one page should be rotated or all the pages).</summary>
      if (this._document && this._document.IsValid) {
         var degree = this._convertToDegrees(rotation);
         this._currentLayout.rotate(degree);
         this._fetchNewImage('rotate');
      }
   },

   //   //////////////// Event Handlers  ////////////////////////////////////////////////////////////////////////////////////////////

   _onMouseDown: function (e) {
      if (e.target != this._container) { return; }
      e.preventDefault();
      if (this._cursor.isPan()) {
         var documentPosition = this._scrollbars.get_position();
         this._cursor.onMouseChanged(e);
         var mousePosition = this._cursor.get_absoluteMousePosition();
         this._cursor.startPan(mousePosition, documentPosition);
      }
   },

   _onMouseMove: function (e) {
      if (this._cursor.isMouseDown()) {
         //if a mouse button is pressed down
         this._cursor.onMouseChanged(e);
         var mouseDownPosition = this._cursor.get_mouseDownPosition();
         var currentMousePosition = this._cursor.get_absoluteMousePosition();

         var position = mouseDownPosition.clone();
         position.add(currentMousePosition);

         var spreadSize = this._currentLayout.calculateSpreadSize();
         var zoom = this._zoom.calculateZoomValue(spreadSize);
         var containerSize = this._currentLayout.calculateContainerSize(zoom);
         // detect if scrollbars are needed by comparing container size with outer div max size
         var viewportSize = this._viewport.get_size();

         //Align the document by the viewport's center if the viewport is wider.
         position.x = this._adjust(viewportSize.width - containerSize.width, position.x);
         position.y = this._adjust(viewportSize.height - containerSize.height, position.y);

         //set the position of the viewport.
         this._viewport.set_position(position);
      }
   },

   _onMouseUp: function (e) {

      this._cursor.onMouseChanged(e);

      var isMouseDown = this._cursor.isMouseDown();

      if (e.target != this._container && !isMouseDown) { return; }

      if (this._cursor.isPan()) {
         this._cursor.endPan();
         this._fetchNewImage('_onMouseUp');

      } else if (this._cursor.isZoomIn()) {
         var relMousePosition = this._cursor.get_relativeMousePosition();
         this._zoom.zoomIn(relMousePosition);
         this._fetchNewImage('_onMouseUp');

      } else if (this._cursor.isZoomOut()) {
         var relMousePosition = this._cursor.get_relativeMousePosition();
         this._zoom.zoomOut(relMousePosition);
         this._fetchNewImage('_onMouseUp');
      }
   },

   _endDrag: function (e) {
      if (this._cursor.isMouseDown()) {
         e.preventDefault();
         this._onMouseUp(e);
      }
   },

   _onScroll: function (e) {
      if (this._delayTimer) {
         clearTimeout(this._delayTimer);
      }
      if (!this._suppressOnScrollEvent) {
         this._delayTimer = setTimeout(Function.createDelegate(this, this._onScrollInternal), 500);
      } else {
         this._suppressOnScrollEvent = false;
      }
   },

   _onScrollInternal: function () {
      var spreadSize = this._currentLayout.calculateSpreadSize();
      var zoom = this._zoom.calculateZoomValue(spreadSize);

      var viewportRect = this._viewport.get_rectangle();

      this._currentLayout.recalculatePageIndexes(zoom, viewportRect);
      this._fetchNewImage('_onScrollInternal');
   },

   _enableScrollEvents: function () {
      this._suppressOnScrollEvent = false;
   },

   _disableScrollEvents: function () {
      this._suppressOnScrollEvent = true;
   },

   _fetchNewImage: function (whereCameFrom) {
      if (this._serviceUrl && this._document.IsValid) {
         //alert('fetch');
         // preload image
         if (null !== this._imgPreload) {
            // preloader is active; dump it
            $clearHandlers(this._imgPreload);
            this._imgPreload = null;
         }
         this._imgPreload = new Image();

         // fire updating event
         var h = this.get_events().getHandler('updating');
         if (h) {
            try {
               h(this, Sys.EventArgs.Empty);
            } catch (ex) { } // try/catch to prevent errors in handlers from breaking the control
         }

         // start preloader
         $addHandler(this._imgPreload, 'load', Function.createDelegate(this, this._preloadHandler));

         //to get a size of the result image, we need to calculate a size of the container
         //the zoom value depends on the size of a spread (specialy FitToPage, FitToWidth etc.)
         var spreadSize = this._currentLayout.calculateSpreadSize();
         var zoom = this._zoom.calculateZoomValue(spreadSize);
         //calculate a size of the conteiner for the given zoom value.
         var containerSize = this._currentLayout.calculateContainerSize(zoom);

         //this._documentContainer.setImageSize(this._documentContainer.getImageUrl(), containerSize);

         //calculate the size of the result image.
         var maxViewportSize = this._viewport.get_maxSize();
         var minViewportSize = this._viewport.get_minSize();
         var imageSize = this._image.calculateSize(containerSize, minViewportSize, maxViewportSize);

         //now we need to get a position of the viewport, to render a needed part of the document
         var documentPosition = this._viewport.get_position();

         //The positioin could be affected by the zooming to a particular point within the document.
         //This point should be as closer to the center of the viewport as it is possible. So we need to move
         //the viewport.
         if (this._zoom.isDocumentPositionAffectedByZoom()) {

            //get the position of the viewport
            documentPosition = this._zoom.calculateDocumentPosition();

            //make sure that this position is in the bounds of the container
            var maxX = minViewportSize.width - containerSize.width;
            var maxY = minViewportSize.height - containerSize.height;

            //prevent going too far horisontally
            if (documentPosition.x > 0) { documentPosition.x = 0; }
            if (documentPosition.x < maxX) { documentPosition.x = maxX; }
            //prevent going too far vertically
            if (documentPosition.y > 0) { documentPosition.y = 0; }
            if (documentPosition.y < maxY) { documentPosition.y = maxY; }

            documentPosition.x = Math.round(documentPosition.x);
            documentPosition.y = Math.round(documentPosition.y);
         }

         //Indexes of the current, the first and the last pages could be changed, so recalculate the indexes.
         var viewportRect = this._viewport.get_rectangle();
         this._currentLayout.recalculatePageIndexes(zoom, viewportRect);

         //Perform a layout operation.
         this._currentLayout.layout(this._document);

         //calculate the displacement of the first page from the top bound of the viewport
         var firstPageIndex = this._currentLayout.get_firstVisiblePageIndex();
         var firstPagePositionY = this._currentLayout.calculatePagePositionY(firstPageIndex, zoom);
         var firstPageDisplacementY = Math.min(0, documentPosition.y - firstPagePositionY);

         //calculate the displacement of the first page from the left bound of the viewport
         var firstPageDisplacementX = (documentPosition.x / zoom).toFixed(0) * zoom;
         firstPageDisplacementX = Math.min(0, firstPageDisplacementX);

         //collect the rest of the information to send to the server
         var lastPageIndex = this._currentLayout.get_lastVisiblePageIndex();
         var layoutName = this._currentLayout.get_name();
         var positions = this._convertPositionInfo(firstPageIndex, lastPageIndex);
         var rotations = this._convertRotationInfo(firstPageIndex, lastPageIndex);
         var horisontalSpacing = this.get_horisontalSpacing();
         var verticalSpacing = this.get_verticalSpacing();

         var modifiedDate = null;

         if (this._document.LastModified instanceof Date)
            modifiedDate = this._document.LastModified.getTime();
         else {
            //in some cases the LastModified is not Date type but string
            //which has a format '/Date(1245398693390)/'
            //therefore we convert it to Date type here
            var date = new Date(parseInt(this._document.LastModified.substr(6)));
            modifiedDate = date.getTime();
         }

         //create a query
         var imageUrl = String.format('{0}?&it=viewer&b={1}&f={2}&g={3}&y={4}&c={5}&r={6}&x={7}&m={8}&a={9}&p={10}&l={11}&j={12}&h={13}&fv={14}',
                                       this._serviceUrl,          //0 - a service which provides an image
                                       this._document.StorageKey, //1 - an id if a pdf document
                                       imageSize.width,           //2
                                       imageSize.height,          //3
                                       firstPageDisplacementY,    //4
                                       zoom,                      //5 - zoom
                                       rotations,                 //6 - rotation info
                                       positions,                 //7 - page positions
                                       firstPageDisplacementX,    //8 - page offset x
                                       firstPageIndex,            //9 - first page index
                                       lastPageIndex,             //10 - last page index
                                       layoutName,                //11 - name of the layout.(actually this value is needed only on the client side to store information about the layout in the image itself)
                                       verticalSpacing,           //12 - j - vertical spacing
                                       horisontalSpacing,          //13 - h - horisontal spacing
                                       modifiedDate           //14 - fv - file version
                                       );

         this._imgPreload.src = imageUrl;
         //alert(String.format('fetch from "{0}"',whereCameFrom));
      }
   },

   _preloadHandler: function (evt) {
      if (null !== this._imgPreload) {
         //alert('preload');

         //get the position of the viewport before the size of the
         //container is changed
         var position = this._viewport.get_position();

         if (this._zoom.isDocumentPositionAffectedByZoom()) {
            //if the position of the viewport was affected by zooming,
            //get the new position and clear information about the position affection in the zoom object.
            position = this._zoom.calculateDocumentPosition();
            this._zoom.markDocumentPositionAsNotAffected();
         }

         //calculate a size of the container
         var spreadSize = this._currentLayout.calculateSpreadSize();
         var zoom = this._zoom.calculateZoomValue(spreadSize);
         var containerSize = this._currentLayout.calculateContainerSize(zoom);

         if (this._isContainerUpdateRequired()) {
            //Modify the size of the container
            this._documentContainer.set_size(containerSize);
         }

         // detect if scrollbars are needed by comparing container size with outer div max size
         var viewportSize = this._viewport.get_size();
         var maxViewportSize = this._viewport.get_maxSize();

         this._scrollbars.enableHorisontal(maxViewportSize.width < (containerSize.width));
         this._scrollbars.enableVertical(maxViewportSize.height < containerSize.height);

         this._background.setSize(Math.max(viewportSize.width, containerSize.width), Math.max(viewportSize.height, containerSize.height));


         //Align the document by the viewport's center if the viewport is wider.
         position.x = this._adjust(viewportSize.width - containerSize.width, position.x);
         position.y = this._adjust(viewportSize.height - containerSize.height, position.y);
         //set the position of the viewport.
         this._viewport.set_position(position);

         //calculate a position of the image within the container.
         var imagePosition = this._viewport.get_position();
         imagePosition.x = viewportSize.width < containerSize.width ? imagePosition.x : 0;
         imagePosition.y = viewportSize.height < containerSize.height ? imagePosition.y : 0;

         //set the image.
         this._documentContainer.setImage(this._imgPreload.src, imagePosition);
         //setup links
         this._linksProvider.setupLinksOnVisiblePages(this._documentContainer, this._currentLayout, zoom);
         //setup search selection
         this._setupSelection(this._selection, zoom);

         $clearHandlers(this._imgPreload);
         this._imgPreload = null;
         // fire updated event
         var h = this.get_events().getHandler('updated');
         if (h) {
            try {
               h(this, Sys.EventArgs.Empty);
            }
            catch (ex) { alert(ex); }
         }
      }
      return false;
   },

   // Search

   search: function (text, matchCase, matchWholeWord) {
      ///<summary>Search for occurance of the specified text in the current document.</summary>
      ///<remarks>Performs an asynchronous request to find the first occurance of the text in the document.
      /// When found the text will be highlighted on the page.
      /// <para>Searching starts on the current page and will continue at the start of the document if the end of the document is reached.</para>
      /// <para>When a search request is started the <see cref="E:J#searching"/> event occures. When the request is completed the <see cref="E:J#searchCompleted"/> occures.
      /// You can use these events to update controls and to indicate a search request is busy.</para>
      /// </remarks>
      /// <example>Search for the word <c>document</c> in the current document.
      /// <code lang="JavaScript">
      /// var viewer = $find('pdfViewer');
      /// var page = viewer.search( 'document' false, false );
      /// </code>
      /// </example>
      /// <param name="text" type="String">The text to search for.</param>
      /// <param name="matchCase" type="Boolean">Set to <c>true</c> to do a case sensitive search.</param>
      /// <param name="matchWholeWord" type="Boolean">Set to <c>true</c> to search for whole words only.</param>
      var doc = this.get_document();
      if (doc && doc.IsValid) {

         var currentPageIndex = this.get_pageIndex();

         var isActiveSearchResults = this._search.results &&
                                    this._search.query.Text == text &&
                                    this._search.query.MatchCase == matchCase &&
                                    this._search.query.MatchWholeWord == matchWholeWord;

         var isPageNotChanged = this._search.results && currentPageIndex == this._search.results[this._search.index].Page;

         if (isActiveSearchResults && isPageNotChanged) {
            //first result is highlighted during the search execution
            //that's why here we start test search results from the second
            if (this._search.index + 1 < this._search.results.length) {
               this._search.index++;

               // highlight next result on current page
               this.highlight(this._search.results[this._search.index].Page, this._search.results[this._search.index].Rectangles, this._searchResultCssClass);
               return;
            }
            else {
               currentPageIndex += 1;
               this._search.lookedPagesCount++;
               if (currentPageIndex == doc.Pages.length) {
                  currentPageIndex = 0;
               }
            }
         }
         else {
            //page was changed, do a new query
            this._search.startPageIndex = currentPageIndex;
            this._search.lookedPagesCount = 0;
         }
         this._executeSearch(doc.StorageKey, this._search.startPageIndex, currentPageIndex, this._search.lookedPagesCount, text, matchCase, matchWholeWord);
      }
   },

   _executeSearch: function (storageKey, startPage, currentPage, prevPage, text, matchCase, matchWholeWord) {
      var service = this.get_searchServiceUrl();
      if (!service) {
         throw Error.invalidOperation("searchServiceUrl property not set. Unable to execute search.");
      }
      // Setup request
      var wRequest = new Sys.Net.WebRequest();
      var searchServiceUrl = this.get_searchServiceUrl();
      wRequest.set_url(searchServiceUrl);
      wRequest.set_httpVerb("POST");
      wRequest.add_completed(Function.createDelegate(this, this._onSearchCompleted));
      var requestBody = String.format('doc={0}&startPage={1}&currentPage={2}&searchedPages={3}&find={4}{5}{6}',
                                       storageKey,
                                       startPage,
                                       currentPage,
                                       prevPage,
                                       text,
                                       requestBody ? "&matchCase=true" : '',
                                       requestBody ? "&matchWholeWord=true" : '');

      wRequest.set_body(requestBody);
      wRequest.get_headers()["Content-Length"] = requestBody.length;
      this._search.results = null;
      this._search.query = { StorageKey: storageKey, Text: text, Request: wRequest, MatchCase: matchCase, MatchWholeWord: matchWholeWord };
      // fire searching event
      var h = this.get_events().getHandler('searching');
      if (h) {
         try {
            h(this, Sys.EventArgs.Empty);
         } catch (ex) { } // try/catch to prevent errors in handlers from breaking the control
      }
      wRequest.invoke();
   },

   cancelSearch: function () {
      ///<summary>Cancel a pending search query.</summary>
      if (this._search.query && this._search.query.Request) {
         var executor = this._search.query.Request.get_executor();
         if (executor.get_started()) {
            executor.abort();
         }
         if (executor.get_aborted()) {
            this._search.query.Request = null;
         }
      }
   },

   _fireSearchCompleted: function (result) {
      // fire searchCompleted event
      var h = this.get_events().getHandler('searchCompleted');
      if (h) {
         try {
            h(this, new TallComponents.SearchCompletedEventArgs(result));
         } catch (ex) { } // try/catch to prevent errors in handlers from breaking the control
      }
   },

   _onSearchCompleted: function (executor, eventArgs) {
      this._search.query.Request = null;

      if (executor.get_responseAvailable()) {
         try {
            var respData = executor.get_responseData();
            this._search.results = eval("(" + respData + ")");
         }
         catch (ex) {
            this._search.results = null;
         }

         if (!this._search.results || !this._search.results.length) {
            this._search.results = null;
            this._search.index = null;

            this._fireSearchCompleted(TallComponents.SearchResultEnum.NotFound);
         }
         else {
            if (this._search.results[0].Restarted == 1) {
               this.set_pageIndex(this._search.startPageIndex);

               this._fireSearchCompleted(TallComponents.SearchResultEnum.SearchComplited);

               this._search.results = null;
               this._search.index = null;

               //remove an old selection
               if (this._selection && this._selection.Rectangles) {
                  this._documentContainer.removeRectangles(this._selectionIds);
               }
               this._selection = { PageIndex: pageIndex, Rectangles: rectangles, CssClass: cssClass };
            }
            else {

               //            this._search.index = this._search.results.length-1;
               //            this.highlight(this._search.results[this._search.index].Page, this._search.results[this._search.index].Rectangles, this._searchResultCssClass);

               this.highlight(this._search.results[0].Page, this._search.results[0].Rectangles, this._searchResultCssClass);
               this._search.index = 0;

               this._fireSearchCompleted(TallComponents.SearchResultEnum.Success);
            }
         }
      }
   },

   highlight: function (pageIndex, rectangles, cssClass) {
      if (rectangles) {
         this._updateSelection = true;

         //remove an old selection
         if (this._selection && this._selection.Rectangles) {
            this._documentContainer.removeRectangles(this._selectionIds);
         }

         this._selection = { PageIndex: pageIndex, Rectangles: rectangles, CssClass: cssClass };

         if (this.get_pageIndex() != pageIndex) {
            this.set_pageIndex(pageIndex);
            //this.goTo(pageIndex, null, null);
         }
         else {
            var spreadSize = this._currentLayout.calculateSpreadSize();
            var zoom = this._zoom.calculateZoomValue(spreadSize);
            this._setupSelection(this._selection, zoom);
         }
      }
   },

   _setupSelection: function (selection, zoom) {
      var firstPageIndex = this._currentLayout.get_firstVisiblePageIndex();
      var lastPageIndex = this._currentLayout.get_lastVisiblePageIndex();

      var isSelectionOnCurrentlyVisiblePages = ((selection != null) && (firstPageIndex <= this._selection.PageIndex) && (this._selection.PageIndex <= lastPageIndex));

      if (isSelectionOnCurrentlyVisiblePages) {
         for (var rectangleIndex = 0; rectangleIndex < selection.Rectangles.length; rectangleIndex++) {

            var page = this._currentLayout.getPageByIndex(selection.PageIndex);
            var pageRectangle = this._currentLayout.getPageRectangle(page);

            var sr = selection.Rectangles[rectangleIndex];
            var selectionRect = new TallComponents.Rectangle(new TallComponents.Vector2(sr.Left, sr.Top), new TallComponents.Size(sr.Width, sr.Height));

            //rotate the selection if the page is rotated
            selectionRect.rotateToParent(page.rotation, pageRectangle);
            //selection rectangle has the coordinates with respect to the page coordinate system,
            //here we convert the coordinates to the container coordinate system
            selectionRect.translateToGlobal(pageRectangle);
            //modify size and location of the selection with respect to current zoom factor.
            selectionRect.scale(zoom);
            selectionRect.location.multiplyByNumber(zoom);

            //put the selection on the container
            this._documentContainer.addRectangle(selectionRect, selection.CssClass, this._selectionIds + rectangleIndex, null, null);

            if (this._updateSelection) {
               this._updateSelection = false;

               var viewportRectangle = this._viewport.get_rectangle();

               if (!viewportRectangle.contains(selectionRect)) {

                  var position = selectionRect.location.clone();

                  var selectionLocalCenter = new TallComponents.Vector2(selectionRect.size.width / 2, selectionRect.size.height / 2);
                  position.add(selectionLocalCenter);

                  var viewportRectangle = this._viewport.get_rectangle();
                  var viewportCenter = new TallComponents.Vector2(viewportRectangle.size.width / 2, viewportRectangle.size.height / 2);
                  position.substract(viewportCenter);

                  viewportRectangle.location = position.clone();
                  pageRectangle.scale(zoom);
                  pageRectangle.location.multiplyByNumber(zoom);

                  if (!pageRectangle.contains(viewportRectangle)) {

                     var right = viewportRectangle.get_rightBottom().x - pageRectangle.get_rightBottom().x;
                     var bottom = viewportRectangle.get_rightBottom().y - pageRectangle.get_rightBottom().y;
                     var left = viewportRectangle.get_leftTop().x - pageRectangle.get_leftTop().x;
                     var top = viewportRectangle.get_leftTop().y - pageRectangle.get_leftTop().y;

                     var offset = new TallComponents.Vector2(0, 0);

                     if (right > 0) {
                        offset.x = right;
                     }
                     if (left < 0) {
                        offset.x = left;
                     }

                     if (bottom > 0) {
                        offset.y = bottom;
                     }
                     if (top < 0) {
                        offset.x = top;
                     }

                     position.substract(offset);
                  }
                  position.multiplyByNumber(-1);
                  this._viewport.set_position(position);
               }
            }
         }
      }
   },

   _link: function (pageIndex, linkIndex) {
      var page = this._document.Pages[pageIndex];
      if (page && page.Links) {
         var link = page.Links[linkIndex];
         if (link) {
            eval(link.Action);
         }
      }
   },

   _isContainerUpdateRequired: function () {
      var oldImageUrl = this._documentContainer.getImageUrl();
      var newImageUrl = this._imgPreload.src;

      var oldImageProperties = this._image.parseImageProperties(oldImageUrl);
      var newImageProperties = this._image.parseImageProperties(newImageUrl);

      //collect information about the changes
      var isPageIndexChanged = oldImageProperties.pageIndex != newImageProperties.pageIndex;
      var isPageLayoutChanged = oldImageProperties.pageLayout != newImageProperties.pageLayout;
      var isVerticalSpacingChanged = oldImageProperties.verticalSpacing != newImageProperties.verticalSpacing;
      var isHorisontalSpacingChanged = oldImageProperties.horisontalSpacing != newImageProperties.horisontalSpacing;
      var isAnotherDocumentOpened = oldImageProperties.key != newImageProperties.key;
      var isZoomChanged = oldImageProperties.zoom != newImageProperties.zoom;
      var isPageRotated = oldImageProperties.rotation != newImageProperties.rotation;

      //should we update the container?
      var isUpdateRequired = isPageIndexChanged || isPageLayoutChanged || isVerticalSpacingChanged || isHorisontalSpacingChanged || isAnotherDocumentOpened || isZoomChanged || isPageRotated;

      return isUpdateRequired;
   },

   _convertToDegrees: function (rotation) {
      var degree = 0;
      switch (rotation) {
         case 'clockwise':
         case 'right':
         case 90:
            degree = 90;
            break;
         case 'counterClockwise':
         case 'left':
         case 270:
            degree = -90;
            break;
         case 180:
            degree = 180;
            break;
         default:
            break;
      }
      return degree;
   },

   _convertPositionInfo: function (firstPageIndex, lastPageIndex) {
      ///<summary>Converts position info of the pages into a string to be used in the request.</summary>
      if (this._document.IsValid) {
         var result = '';

         var page = this._document.Pages[firstPageIndex];

         var offset = page.Top;

         if (firstPageIndex < lastPageIndex) {
            var page2 = this._document.Pages[firstPageIndex + 1];
            offset = Math.min(page.Top, page2.Top);
         }

         for (var pageIndex = firstPageIndex; pageIndex <= lastPageIndex; pageIndex++) {

            page = this._document.Pages[pageIndex];
            result += String.format('{0}:{1};', page.Left, page.Top - offset);
         }
         return result;
      }
      return '0:0;';
   },

   _convertRotationInfo: function (firstPageIndex, lastPageIndex) {
      ///<summary>Converts the pages rotation information in a specific format for sending to the server.</summary>
      ///<remarks>Actually the method forms a string with a hex representation of rotation information for all the pages
      /// in a range from firstPageIndex to lastPageIndex. Each two bits represent a rotation information for a certain page.
      /// 00 - no rotation
      /// 01 - 90 degree rotation
      /// 10 - 180 degree rotation
      /// 11 - 270 degree rotation </remarks>

      //is document ready?
      if (!this._document.IsValid) {
         //then no rotation
         return '0';
      }
      var bin = '';
      //form the string with binary data
      for (i = firstPageIndex; i <= lastPageIndex; i++) {
         var page = this._document.Pages[i];
         var rotation = page.rotation ? page.rotation : 0;

         switch (rotation) {
            case 90:
               bin += '01';
               break;
            case 180:
               bin += '10';
               break;
            case 270:
               bin += '11';
               break;
            default:
               bin += '00';
               break;
         }
      }
      //convert the binary data to hex
      var dec = parseInt(bin, 2);
      var hex = dec.toString(16);

      return hex;
   },

   _adjust: function (delta, x) {
      if (delta >= 0) {
         x = Math.round(delta / 2);
      }
      else {
         if (x > 0) { x = 0; }
         if (x < delta) { x = delta; }
      }
      return x;
   },


   // Bind and unbind to events.
   add_updated: function (handler) {
      ///<summary>Occures when the viewer finishes an update.</summary>
      ///<value type="Function">A handler function for <c>updated</c> event</value>
      ///<remarks>The handler signature should be:
      ///<code lang="JavaScript">function myHandler( sender, args )</code>
      ///where <c>sender</c> is the PdfViewer that fired the event and <c>args</c> is an empty <see cref="T:J#Sys.EventArgs"/> instance. 
      ///The handler should query the sender control to determine the status of the control.
      ///</remarks>
      this.get_events().addHandler('updated', handler);
   },
   remove_updated: function (handler) {
      this.get_events().removeHandler('updated', handler);
   },
   add_updating: function (handler) {
      ///<summary>Occures when the viewer starts an update.</summary>
      ///<value type="Function">A handler function for the <c>updating</c> event</value>
      ///<remarks>The handler signature should be:
      ///<code lang="JavaScript">function myHandler( sender, args )</code>
      ///where <c>sender</c> is the PdfViewer that fired the event and <c>args</c> is an empty <see cref="T:J#Sys.EventArgs"/> instance. 
      ///The handler should query the sender control to determine the status of the control.
      ///</remarks>
      this.get_events().addHandler('updating', handler);
   },
   remove_updating: function (handler) {
      this.get_events().removeHandler('updating', handler);
   },

   add_searchCompleted: function (handler) {
      ///<summary>Occures when the viewer finishes searching.</summary>
      ///<value type="Function">A handler function for <c>searchCompleted</c> event</value>
      ///<remarks>The handler signature should be:
      ///<code lang="JavaScript">function myHandler( sender, args )</code>
      ///where <c>sender</c> is the PdfViewer that fired the event and <c>args</c> is  a <see cref="T:J#TallComponents.SearchCompletedEventArgs"/> instance. 
      ///</remarks>
      this.get_events().addHandler('searchCompleted', handler);
   },
   remove_searchCompleted: function (handler) {
      this.get_events().removeHandler('searchCompleted', handler);
   },
   add_searching: function (handler) {
      ///<summary>Occures when the viewer starts a search query.</summary>
      ///<value type="Function">A handler function for the <c>searching</c> event</value>
      ///<remarks>The handler signature should be:
      ///<code lang="JavaScript">function myHandler( sender, args )</code>
      ///where <c>sender</c> is the PdfViewer that fired the event and <c>args</c> is an empty <see cref="T:J#Sys.EventArgs"/> instance. 
      ///</remarks>
      this.get_events().addHandler('searching', handler);
   },
   remove_searching: function (handler) {
      this.get_events().removeHandler('searching', handler);
   },


   get_isBusy: function () {
      ///<summary>Gets a value that indicates if the control is performing a network request.</summary>
      ///<remarks>Network requests like generating a preview or searching can take some time. This value indicates 
      /// whether the control is busy performing a network request. This can be used to show a busy indicator to the user.</remarks>
      ///<value type="Boolean"><c>true</c> if the control is busy, otherwise <c>false</c>.</value>
      return (null !== this._imgPreload || (this._search.query && this._search.query.Request));
   },

   getPageRotation: function (pageIndex) {
      var rotation = 0;
      if (this._document.IsValid && this._document.Pages.length > pageIndex) {
         var page = this._document.Pages[pageIndex];
         if (!isNaN(page.rotation)) {
            rotation = page.rotation;
         }
      }
      return rotation;
   },

   getPageSize: function (pageIndex) {
      var size = { width: 0, height: 0 };
      if (this._document.IsValid && this._document.Pages.length > pageIndex) {
         var page = this._document.Pages[pageIndex];
         size = this._currentLayout.getPageOriginalSize(page);
      }
      return size;
   },

   get_pageIndex: function () {
      ///<summary>Gets or sets the 0-based index of the page to display.</summary>
      ///<remarks>Setting this value will start updating the viewer immediatly.
      ///The image for the new page may take some time to load though.</remarks>
      ///<value type="Number" integer="true">The 0-based index of the current page.</value>
      return this._currentLayout.get_currentPageIndex();
   },

   set_pageIndex: function (value) {
      this._internalSetPageIndex(value, true);
   },

   _internalSetPageIndex: function (newPageIndex, reloadPositionAndImage) {
      var currentPageIndex = this._currentLayout.get_currentPageIndex();
      if (currentPageIndex != newPageIndex) {
         this._currentLayout.set_currentPageIndex(newPageIndex);
         this.raisePropertyChanged('pageIndex');
         if ((null !== this._container) && (reloadPositionAndImage)) {
            this._updateViewportPosition();
            this._fetchNewImage('_internalSetPageIndex');
         }
      }
   },

   get_pageLayout: function () {
      ///<summary>Gets or sets the page layout on this view.</summary>
      ///<remarks>Setting this value will start updating the viewer immediatly.
      ///The image for the new page may take some time to load though.</remarks>
      ///<value type="String">The 0-based index of the current page.</value>

      return this._currentLayout.get_name();
   },

   set_pageLayout: function (value) {
      var name = this._currentLayout.get_name();
      if (name != value) {
         this.raisePropertyChanged('pageLayout');
         if (null !== this._container) {
            this._currentLayout = this._layoutFactory.getByName(value);

            this._updateViewportPosition();

            var spreadSize = this._currentLayout.calculateSpreadSize();
            var zoom = this._zoom.calculateZoomValue(spreadSize);

            var viewportRect = this._viewport.get_rectangle();
            this._currentLayout.recalculatePageIndexes(zoom, viewportRect);
            this._fetchNewImage('set_pageLayout');
         }
      }
   },

   _updateViewportPosition: function () {
      //get zoom which is needed to get page position and container size
      var spreadSize = this._currentLayout.calculateSpreadSize();
      var zoom = this._zoom.calculateZoomValue(spreadSize);

      //get current page (this could be specified)
      var pageIndex = this._currentLayout.get_currentPageIndex();
      //get documtent position, also could be got in params
      var documentPosition = this._currentLayout.calculatePagePositionY(pageIndex, zoom);
      var documentPositionX = this._currentLayout.calculatePagePositionX(pageIndex, zoom);

      var containerSize = this._currentLayout.calculateContainerSize(zoom);
      var maxViewportSize = this._viewport.get_maxSize();
      var viewportSize = this._viewport.get_size();

      //update the containers
      this._documentContainer.set_size(containerSize);
      this._scrollbars.enableHorisontal(maxViewportSize.width < containerSize.width);
      this._scrollbars.enableVertical(maxViewportSize.height < containerSize.height);
      this._background.setSize(Math.max(viewportSize.width, containerSize.width), Math.max(viewportSize.height, containerSize.height));

      var maxPosition = -1 * (containerSize.height - viewportSize.height);

      if (documentPosition < maxPosition) {
         documentPosition = maxPosition
      }

      var maxPositionX = -1 * (containerSize.width - viewportSize.width);

      if (documentPositionX < maxPositionX) {
         documentPositionX = maxPositionX
      }

      documentPosition = this._adjust(viewportSize.height - containerSize.height, documentPosition);
      documentPositionX = this._adjust(viewportSize.width - containerSize.width, documentPositionX);

      this._viewport.set_positionY(documentPosition);
      this._viewport.set_positionX(documentPositionX);
   },

   get_verticalSpacing: function () {
      ///<summary>Gets or sets the vertical space in pixels between 2 pages.</summary>
      ///<remarks>Setting this value will start updating the viewer immediatly.
      ///The image for the new page may take some time to load though.</remarks>
      ///<value type="Number">The vertical space in pixels between 2 pages.</value>
      return this._verticalSpacing;
   },

   set_verticalSpacing: function (value) {
      if (this._verticalSpacing != value) {
         this._verticalSpacing = value;
         this.raisePropertyChanged('verticalSpacing');
         if (null !== this._container) {
            this._fetchNewImage('set_verticalSpacing');
         }
      }
   },

   get_horisontalSpacing: function () {
      ///<summary>Gets or sets the horisontal space in pixels between 2 pages.</summary>
      ///<remarks>Setting this value will start updating the viewer immediatly.
      ///The image for the new page may take some time to load though.</remarks>
      ///<value type="Number">The horisontal space in pixels between 2 pages.</value>
      return this._horisontalSpacing;
   },

   set_horisontalSpacing: function (value) {
      if (this._horisontalSpacing != value) {
         this._horisontalSpacing = value;
         this.raisePropertyChanged('horisontalSpacing');
         if (null !== this._container) {
            this._fetchNewImage('set_horisontalSpacing');
         }
      }
   },

   get_pageCount: function () {
      ///<summary>Gets the total number of pages in the current document.</summary>
      ///<value type="Number" integer="true">The total number of pages in the current document.</value>
      return (this._document && this._document.IsValid ? this._document.Pages.length : 0);
   },

   get_zoom: function () {
      ///<summary>Gets or sets the current zoom.</summary>
      ///<remarks>Setting this value will start updating the viewer immediatly. 
      ///The image for the new page may take some time to load though.</remarks>
      ///<value type="String">This is either a floating point value (e.g. 0.75 is 75%) or a preset zoom factor ( fitToPage, fitToWidth ).</value>
      return this._zoom.getValueAsString();
   },

   set_zoom: function (value) {
      var zoomAsString = this._zoom.getValueAsString();
      if (value != zoomAsString) {
         this._zoom.setValue(value);
         this._fetchNewImage('set_zoom');
      }
      else {
         return; //the value has been already set
      }
   },

   get_zz_serviceUrl: function () {
      ///<summary>Gets or sets the URL of the service that renders (partial) PDF pages.</summary>
      ///<value type="String">The URL of the service that renders (partial) PDF pages.</value>
      //This property has a strange name on purpose. The value will be set at the last moment to this property.
      //So fetchNewImage will not be called during setting of another properties.
      return this._serviceUrl;
   },

   set_zz_serviceUrl: function (value) {
      if (this._serviceUrl != value) {
         this._serviceUrl = value;
         this.raisePropertyChanged('serviceUrl');
         this._fetchNewImage();
      }
   },

   get_searchServiceUrl: function () {
      ///<summary>Gets or sets the URL of the service that enables full-text searching in PDF documents.</summary>
      ///<value type="String">The URL of the service that enables full-text searching in PDF documents.</value>
      return this._searchServiceUrl;
   },

   set_searchServiceUrl: function (value) {
      if (this._searchServiceUrl != value) {
         this._searchServiceUrl = value;
         this.raisePropertyChanged('searchServiceUrl');
      }
   },

   get_searchResultCssClass: function () {
      ///<summary>Gets or sets the CSS class applied to the rectangle that highlights search results.</summary>
      ///<value type="String">The  CSS class applied to the rectangle that highlights search results.</value>
      return this._searchResultCssClass;
   },

   set_searchResultCssClass: function (value) {
      if (this._searchResultCssClass != value) {
         this._searchResultCssClass = value;
         this.raisePropertyChanged('searchResultCssClass');
      }
   },

   get_document: function () {
      /// <summary>Gets or sets the current document.</summary>
      /// <value type="Object">The current document.</value>
      /// <remarks>This property is set at initialization by script injected by the server control.
      /// <para>The document object has the following properties:</para>
      /// <para><c>StorageKey</c> the document storage key.</para>
      /// <para><c>IsValid</c> gets a value indicating if the document is loaded and ready for use.</para>
      /// <para><c>Pages</c> an array of Page objects.</para>
      /// </remarks>
      return this._document;
   },

   set_document: function (value) {
      if (this._document != value || null === value) {
         if (null === value) {
            this._document = new Object();
            this._document.StorageKey = '';
            this._document.IsValid = false;
            this._document.Pages = Array();
         }
         else {
            this._document = value;
            this._document.IsValid = true;
            var page = this._document.Pages[0];
         }
         this._currentLayout.set_document(this._document);
         this._layoutFactory.set_document(this._document);
         this.raisePropertyChanged('document');
      }
   },

   get_cursorMode: function () {
      ///<summary>Gets or sets the cursor mode.</summary>
      ///<value type="String">The cursor mode.</value>
      ///<remarks>Valid cursor modes are <c>pan</c>, <c>zoomIn</c> and <c>zoomOut</c>. These values are case sensitive.</remarks>
      return this._cursor.get_mode();
   },

   set_cursorMode: function (value) {
      this._cursor.set_mode(value);
      this.raisePropertyChanged('cursorMode');
   },

   get_cursors: function () {
      ///<summary>Gets or sets the list of defined cursors.</summary>
      ///<value type="Array" elementType="String">The list of cursors.</value>
      ///<remarks>This is an associative array and is initialized by the server control.
      ///<para>Valid indexes are <c>panUp</c>, <c>panDown</c>, <c>zoomIn</c> and <c>zoomOut</c>.
      ///Each value holds a url for a custom cursor image.
      ///<c>panUp</c> is used when <see cref="M:J#TallComponents.PdfViewer.cursorMode"/> is <c>pan</c> and the mouse button is up.
      ///<c>panDown</c> is used when the mouse is down and the mouse is dragging the PDF preview. <c>zoomIn</c> and <c>zoomOut</c> are used in 
      ///the corresponding cursor modes.</para>
      ///<para>Custom cursors may not be supported in all browsers (Opera 9.64 and earlier do not support this).</para>
      ///</remarks>
      return this._cursor.get_cursors();
   },

   set_cursors: function (value) {
      this._cursor.set_cursors(value);
   },

   get_size: function () {
      ///<summary>Gets or sets the viewer size.</summary>
      ///<value type="Object">The new size.</value>
      ///<remarks>The size is an object with the width and height properties set to the width and height in pixels.
      ///<example>Resize the viewer to 500 pixels wide and 300 pixels high.
      ///<code lang="JavaScript">
      /// var viewer = $find('pdfViewer');
      /// viewer.set_size({ width : 500, height: 300 });
      /// </code>
      ///</example></remarks>
      return this._viewport.get_size();
   },

   set_size: function (size) {
      var viewportSize = this._viewport.get_size();

      if (size.width) {
         viewportSize.width = parseInt(size.width);
      }
      if (size.height) {
         viewportSize.height = parseInt(size.height);
      }
      var isSizeChanged = this._viewport.set_size(viewportSize);
      if (isSizeChanged) {
         this._fetchNewImage('set_size');
      }
   }
};

// Optional descriptor for JSON serialization.
TallComponents.PdfViewer.descriptor = {
   properties: [{ name: "pageIndex", type: Number },
                 { name: "pageLayout", type: String },
                 { name: "verticalSpacing", type: Number },
                 { name: "horisontalSpacing", type: Number },
                 { name: "zoom", type: String },
                 { name: "searchServiceUrl", type: String },
                 { name: "document", type: Object },
                 { name: "searchResultCssClass", type: String },
                 { name: "zz_serviceUrl", type: String },
                 { name: "cursors", type: Object}]

};
// Register the class as a type that inherits from Sys.UI.Control.
TallComponents.PdfViewer.registerClass('TallComponents.PdfViewer', Sys.UI.Control);


TallComponents.SearchResultEnum = {SearchComplited: 0, Success: 1, NotFound: 2};

TallComponents.SearchCompletedEventArgs = function (searchResult) {
   /// <summary>Event arguments for the <see cref="E:J#TallComponents.PdfViewer.searchCompleted"/> event.</summary>
   if (arguments.length !== 1) { throw Error.parameterCount(); }
   TallComponents.SearchCompletedEventArgs.initializeBase(this);
   this._found = (searchResult == TallComponents.SearchResultEnum.SearchComplited) || (searchResult == TallComponents.SearchResultEnum.Success);
   //alert(String.format("this._found: {0}\nsearchResult: {1}",this._found, searchResult));
   this._searchResult = searchResult;
};
TallComponents.SearchCompletedEventArgs.prototype = {
   get_found: function () {
      ///<summary>Gets a value indicating if the search returned any results.</summary>
      ///<value type="Boolean"><c>true</c> if the search returned any results.</value>
      return this._found;
   },

   get_isSearchComplited: function () {
      ///<summary>Gets a value indicating if the search has been performed for the whole document and returned to the point where it was started.</summary>
      ///<value type="Boolean"><c>true</c> if the search has been performed for the whole document.</value>
      return this._searchResult == TallComponents.SearchResultEnum.SearchComplited;
   },

   get_isSearchSucceeded: function () {
      ///<summary>Gets a value indicating if the search returned any results.</summary>
      ///<value type="Boolean"><c>true</c> if the search returned any results.</value>
      return this._searchResult == TallComponents.SearchResultEnum.Success;
   }

};

TallComponents.SearchCompletedEventArgs.registerClass('TallComponents.SearchCompletedEventArgs', Sys.EventArgs);

if (typeof (Sys) !== 'undefined') { Sys.Application.notifyScriptLoaded(); }
